Skip to main content

azul_layout/
lib.rs

1//! Layout crate for the Azul GUI framework.
2//!
3//! Provides the layout solver (`solver3`), text shaping (`text3`), font
4//! management (`font`), hit testing, page fragmentation, and widget support.
5//! Integrates with `azul-core` for DOM types and `azul-css` for style
6//! properties.
7
8#![doc(
9    html_logo_url = "https://raw.githubusercontent.com/maps4print/azul/master/assets/images/azul_logo_full_min.svg.png",
10    html_favicon_url = "https://raw.githubusercontent.com/maps4print/azul/master/assets/images/favicon.ico"
11)]
12// Lint policy: deny correctness/safety issues, warn on style (`clippy::all`).
13//
14// Crate-wide allows are intentionally limited to lints that are either
15//   (a) pervasive AND feature-sensitive — an import/binding/field that is unused
16//       under one feature set is live under another, so a per-site fix would
17//       break a different feature build — or
18//   (b) churny / newer-toolchain lints with little value in scoping.
19// Lints that fire in only a few, well-localized places are scoped with
20// `#[allow(...)]` on the specific `pub mod` declarations further down, so the
21// rest of the (hand-written) crate is actually checked.
22#![deny(unused_must_use)]
23#![warn(clippy::all)]
24// Extreme-lint lockdown: all clippy groups plus opt-in rustc lints, enforced as
25// -D warnings on library code by the CI clippy job. Test builds are exempt via
26// cfg(not(test)) below since the set is high-noise and low-value on unit and
27// generated tests; clippy::all correctness still applies to test code.
28#![cfg_attr(not(test), warn(
29    clippy::pedantic,
30    clippy::nursery,
31    clippy::cargo,
32    // missing_docs,  // TODO(docs): re-enable as a dedicated final docs pass; disabled
33    //                // for now so the cleanup focuses on code-quality lints, not doc debt.
34    missing_debug_implementations,
35    missing_copy_implementations,
36    unreachable_pub,
37    unused_qualifications,
38    unused_lifetimes,
39    unused_import_braces,
40    unused_macro_rules,
41    unused_crate_dependencies,
42    meta_variable_misuse,
43    trivial_casts,
44    trivial_numeric_casts,
45    elided_lifetimes_in_paths,
46    single_use_lifetimes,
47    variant_size_differences,
48    non_ascii_idents,
49    unsafe_op_in_unsafe_fn,
50    let_underscore_drop,
51))]
52#![allow(
53    // `unknown_lints` lets the two forward-compat lints below be listed even on
54    // the CI toolchain (1.88), where they are not yet known, without emitting an
55    // "unknown lint" warning of their own. They still apply on newer rustc.
56    unknown_lints,
57    mismatched_lifetime_syntaxes,          // newer rustc; fires in macro-generated code
58    function_casts_as_integer,             // newer rustc; widget callback pointer identity
59    // pervasive + feature-sensitive (unused under one feature, live under another):
60    unused_imports,
61    unused_variables,
62    unused_mut,
63    dead_code,
64    // design lint, pervasive across the layout solver / renderer:
65    clippy::too_many_arguments,
66    // churny / 3rd-party, low value to scope:
67    clippy::legacy_numeric_constants,
68    unexpected_cfgs,                        // web-lift diagnostic cfgs
69    deprecated,                             // image crate tiff encoder (only under `tiff`)
70    // transitive dependency-version dups not resolvable in azul's source —
71    // syn 1↔2 (proc-macro migration), heck/jni-sys/rustc-hash/rustls-webpki;
72    // re-audit when the dep tree aligns.
73    clippy::multiple_crate_versions,
74)]
75
76#[macro_use]
77extern crate alloc;
78extern crate core;
79
80// Dependencies kept for downstream/feature-plumbing use but not referenced
81// directly in this crate's source — marked intentionally linked so
82// unused_crate_dependencies stays quiet (the lint's own suggested fix).
83// `brotli-decompressor`: decompresses the codegen material_icons.ttf.br in azul-dll.
84#[cfg(feature = "icons")]
85use brotli_decompressor as _;
86// `lru`: reserved for the slippy-map tile cache (azul-dll widgets).
87use lru as _;
88// `unicode-normalization` / `xmlwriter`: pulled by text_layout / xml for the
89// shaping + SVG-writer paths consumed downstream.
90#[cfg(feature = "text_layout")]
91use unicode_normalization as _;
92#[cfg(feature = "xml")]
93use xmlwriter as _;
94
95/// Web-lift diagnostic marker: a volatile store of `val` to the absolute wasm
96/// linear-memory address `addr` (the 0x40000–0xF0000 free band the e2e harness
97/// peeks via `AzStartup_peekU32`).
98///
99/// Compiles to NOTHING without the `web_lift`
100/// feature — absolute-address stores would segfault native builds (macOS
101/// `__PAGEZERO` covers the low 4 GiB). All in-tree diagnostic markers MUST go
102/// through this helper rather than calling `core::ptr::write_volatile` on a
103/// literal address directly.
104///
105/// # Safety
106///
107/// With the `web_lift` feature enabled, `addr` must be a valid, writable wasm
108/// linear-memory address (within the 0x40000–0xF0000 diagnostic band). Without
109/// the feature this is a no-op and always safe.
110#[cfg(feature = "web_lift")]
111#[inline]
112pub unsafe fn az_mark(_addr: u32, _val: u32) {
113    // Volatile isn't const-callable, so this variant is a plain (non-const) fn.
114    core::ptr::write_volatile(_addr as usize as *mut u32, _val);
115}
116/// No-op `const` variant used when the `web_lift` feature is off.
117///
118/// # Safety
119///
120/// Always safe — this variant does nothing; the `unsafe` marker only exists to
121/// keep the signature identical to the `web_lift` variant so call sites compile
122/// unchanged under both features.
123#[cfg(not(feature = "web_lift"))]
124#[inline]
125pub const unsafe fn az_mark(_addr: u32, _val: u32) {}
126
127/// Read counterpart of [`az_mark`] (marker counters like `0x60758`).
128/// Returns 0 without the `web_lift` feature.
129///
130/// # Safety
131///
132/// With the `web_lift` feature enabled, `addr` must be a valid, readable wasm
133/// linear-memory address (within the 0x40000–0xF0000 diagnostic band). Without
134/// the feature this is a no-op that returns 0 and is always safe.
135#[cfg(feature = "web_lift")]
136#[inline]
137#[must_use] pub unsafe fn az_mark_read(_addr: u32) -> u32 {
138    // Volatile isn't const-callable, so this variant is a plain (non-const) fn.
139    core::ptr::read_volatile(_addr as usize as *const u32)
140}
141/// No-op `const` variant (returns 0) used when the `web_lift` feature is off.
142///
143/// # Safety
144///
145/// Always safe — returns 0 and touches nothing; the `unsafe` marker only exists
146/// to keep the signature identical to the `web_lift` variant.
147#[cfg(not(feature = "web_lift"))]
148#[inline]
149#[must_use] pub const unsafe fn az_mark_read(_addr: u32) -> u32 {
150    0
151}
152
153/// Font traits available regardless of text layout feature.
154pub mod font_traits;
155/// Optional probe instrumentation. With the `probe` feature off this
156/// is a tiny module of no-op stubs and pays zero cost.
157pub mod probe;
158/// Image decoding and encoding (wraps the `image` crate).
159#[cfg(feature = "image_decoding")]
160pub mod image;
161/// Scroll, hover, clipboard, cursor, and focus managers.
162#[cfg(feature = "text_layout")]
163// Scoped (was crate-wide): internal manager types exposed for tests.
164#[allow(private_interfaces)]
165pub mod managers;
166/// CSS layout solver: block, inline, flex, grid, and table formatting.
167#[cfg(feature = "text_layout")]
168// Scoped (was crate-wide): solver internals — intentional `drop(&_)` scope
169// markers, internal types exposed for tests, incremental-relayout assignments,
170// generated/parenthesized property code, and exhaustive generated matches.
171#[allow(
172    dropping_references,
173    private_interfaces,
174    unreachable_patterns,
175    unused_parens,
176    unused_doc_comments,
177    unused_assignments
178)]
179pub mod solver3;
180
181/// C-compatible string formatting via `strfmt`.
182#[cfg(feature = "strfmt")]
183pub mod fmt;
184#[cfg(feature = "strfmt")]
185pub use fmt::{FmtArg, FmtArgVec, FmtArgVecDestructor, FmtValue, fmt_string};
186
187/// Built-in widgets: button, text input, tabs, tree view, node graph, etc.
188#[cfg(feature = "widgets")]
189// Scoped (was crate-wide): incremental widget-state assignments and the
190// node_graph extern "C" fn that returns `()`.
191#[allow(unused_assignments, improper_ctypes_definitions)]
192pub mod widgets;
193
194/// Desktop platform helpers (file dialogs, notifications).
195#[cfg(feature = "extra")]
196pub mod desktop;
197
198/// ICU internationalization: date/time formatting, plurals, list formatting.
199#[cfg(any(
200    feature = "icu",
201    all(target_os = "macos", feature = "icu_macos"),
202    all(target_os = "windows", feature = "icu_windows"),
203))]
204pub mod icu;
205#[cfg(any(
206    feature = "icu",
207    all(target_os = "macos", feature = "icu_macos"),
208    all(target_os = "windows", feature = "icu_windows"),
209))]
210pub use icu::{
211    DateTimeFieldSet, FormatLength, IcuDate, IcuDateTime, IcuError,
212    IcuLocalizer, IcuLocalizerHandle, IcuResult, IcuStringVec, IcuTime,
213    LayoutCallbackInfoIcuExt, ListType, PluralCategory,
214};
215
216/// Project Fluent localization: message bundles, argument formatting, ZIP I/O.
217#[cfg(feature = "fluent")]
218pub mod fluent;
219#[cfg(feature = "fluent")]
220pub use fluent::{
221    check_fluent_syntax, check_fluent_syntax_bytes, create_fluent_zip,
222    create_fluent_zip_from_strings, export_to_zip, FluentError,
223    FluentLanguageInfo, FluentLanguageInfoVec, FluentLoadError, FluentLoadErrorVec,
224    FluentLocalizerHandle, FluentSyntaxCheckResult,
225    FluentZipLoadResult,
226};
227
228/// URL parsing (RFC 3986 compliant). Pure-Rust, always present (no TLS deps).
229/// URL types live in `azul_core::url`; re-exported so `azul_layout::url::*`
230/// keeps resolving. `Url::parse`/`join` are enabled via the `http` feature
231/// (which turns on `azul-core/url`).
232pub use azul_core::url;
233pub use azul_core::url::{Url, UrlParseError, ResultUrlUrlParseError};
234
235/// File system operations (C-compatible wrappers for `std::fs`).
236// Scoped (was crate-wide): `///` doc comments before `impl_vec!`/`impl_option!`
237// macro invocations, and an infallible inherent `FilePath::from_str` (returns
238// `Self`, so it cannot implement the fallible `FromStr` trait).
239#[allow(unused_doc_comments, clippy::should_implement_trait)]
240pub mod file;
241pub use file::{
242    dir_create, dir_create_all, dir_list, dir_delete, dir_delete_all,
243    file_append, file_copy, path_exists, file_metadata, file_read, file_read_string,
244    file_delete, file_rename, file_write, file_write_string,
245    path_canonicalize, path_extension, path_file_name, path_is_dir, path_is_file,
246    path_join, path_parent, temp_dir,
247    DirEntry, DirEntryVec, DirEntryVecDestructor, DirEntryVecDestructorType,
248    FileError, FileErrorKind, FileMetadata, FilePath, OptionFilePath,
249};
250
251/// HTTP client: GET/POST requests with pure-Rust TLS.
252///
253/// API surface always present (stub when off); ureq/rustls only pulled in with `http`.
254pub mod http;
255pub use http::{
256    download_bytes, download_bytes_with_config, http_get,
257    http_get_with_config, is_url_reachable, HttpError, HttpHeader,
258    HttpRequestConfig, HttpResponse, HttpResponseTooLargeError, HttpResult,
259    HttpStatusError,
260};
261
262/// JSON parsing and serialization for the C API.
263#[cfg(feature = "json")]
264pub mod json;
265#[cfg(feature = "json")]
266pub use json::{
267    json_parse, json_stringify,
268    Json, JsonInternal, JsonKeyValue, JsonKeyValueVec, JsonKeyValueVecDestructor, JsonKeyValueVecDestructorType,
269    JsonParseError, JsonType, JsonVec,
270    ResultJsonJsonParseError, OptionJson, OptionJsonVec, OptionJsonKeyValueVec,
271};
272
273/// ZIP file creation, extraction, and listing.
274#[cfg(feature = "zip")]
275pub mod zip;
276#[cfg(feature = "zip")]
277pub use zip::{
278    zip_create, zip_create_from_files, zip_extract_all, zip_list_contents,
279    ZipFile, ZipFileEntry, ZipFileEntryVec, ZipPathEntry, ZipPathEntryVec,
280    ZipReadConfig, ZipWriteConfig, ZipReadError, ZipWriteError,
281};
282
283/// Icon provider: resolves icons from Material Icons font, images, or ZIP packs.
284pub mod icon;
285pub use icon::{
286    // Resolver
287    default_icon_resolver,
288    // Data types for RefAny
289    ImageIconData, FontIconData,
290    // Helpers
291    register_image_icon,
292    register_font_icon,
293    register_icons_from_zip,
294    create_default_icon_provider,
295    register_material_icons,
296    register_embedded_material_icons,
297};
298// Re-export core icon types
299pub use azul_core::icon::{
300    IconProviderHandle, IconResolverCallbackType,
301    resolve_icons_in_styled_dom, OptionIconProviderHandle,
302};
303
304/// Callback handling for layout events (invocation, result processing).
305#[cfg(feature = "text_layout")]
306pub mod callbacks;
307/// CPU-based software rendering (no GPU required).
308#[cfg(feature = "cpurender")]
309// Scoped (was crate-wide): complex rasterizer signatures.
310#[allow(clippy::type_complexity)]
311pub mod cpurender;
312/// Glyph path and cell cache for CPU text rendering.
313#[cfg(feature = "cpurender")]
314pub mod glyph_cache;
315/// Default keyboard actions (copy, paste, select-all, undo, etc.).
316#[cfg(feature = "text_layout")]
317pub mod default_actions;
318/// Event determination: maps raw input to DOM node callbacks.
319#[cfg(feature = "text_layout")]
320pub mod event_determination;
321/// Font parsing, metrics extraction, and subsetting.
322#[cfg(feature = "text_layout")]
323// Scoped (was crate-wide): complex font-table signatures.
324#[allow(clippy::type_complexity)]
325pub mod font;
326
327/// Headless backend for CPU-only rendering without a display server.
328///
329/// Used with `AZUL_HEADLESS=1` for E2E testing, CI, and screenshot capture.
330#[cfg(feature = "text_layout")]
331pub mod headless;
332// Re-export allsorts types needed by printpdf
333#[cfg(feature = "text_layout")]
334pub use allsorts::subset::CmapTarget;
335#[cfg(feature = "text_layout")]
336pub use font::parsed::{
337    FontParseWarning, FontParseWarningSeverity, FontType, OwnedGlyph, ParsedFont, PdfFontMetrics,
338    SubsetFont,
339};
340// Re-export hyphenation for external crates (like printpdf)
341#[cfg(feature = "text_layout_hyphenation")]
342pub use hyphenation;
343/// Hit-testing: maps screen coordinates to DOM nodes.
344#[cfg(feature = "text_layout")]
345pub mod hit_test;
346/// Paged media: the `FragmentationContext` (continuous vs. paged) and page margins.
347/// The primitive types live in `azul_core::paged`; re-exported here so existing
348/// `azul_layout::paged::*` / `crate::paged::*` paths keep resolving.
349pub use azul_core::paged;
350/// Text shaping, line breaking (Knuth-Plass), and inline formatting.
351#[cfg(feature = "text_layout")]
352// Scoped (was crate-wide): internal types exposed for tests, a labelled
353// shaping loop, and complex shaping/cache signatures.
354#[allow(private_interfaces, unused_labels, clippy::type_complexity)]
355pub mod text3;
356/// Thread callback wrappers for the C API.
357#[cfg(feature = "text_layout")]
358pub mod thread;
359/// Timer callback wrappers for the C API.
360#[cfg(feature = "text_layout")]
361// Scoped (was crate-wide): hand-written `Ord`/`PartialOrd` on a timer type.
362#[allow(clippy::non_canonical_partial_ord_impl)]
363pub mod timer;
364/// Scroll physics timer for momentum-based smooth scrolling.
365#[cfg(feature = "text_layout")]
366pub mod scroll_timer;
367/// Window layout management: relayout, event processing, state sync.
368#[cfg(feature = "text_layout")]
369// Scoped (was crate-wide): parenthesized layout expressions.
370#[allow(unused_parens)]
371pub mod window;
372/// Window state types (keyboard, mouse, DPI, focus).
373#[cfg(feature = "text_layout")]
374pub mod window_state;
375/// XML and XHTML parsing for declarative UI definitions.
376#[cfg(feature = "xml")]
377// Scoped (was crate-wide): incremental parser-state assignments.
378#[allow(unused_assignments)]
379pub mod xml;
380
381// Export the main layout function and window management
382/// Canonical paged-media page margins (defined in [`paged`]).
383pub use paged::PageMargins;
384#[cfg(feature = "text_layout")]
385pub use hit_test::{CursorTypeHitTest, FullHitTest};
386#[cfg(feature = "text_layout")]
387pub use solver3::cache::LayoutCache as Solver3LayoutCache;
388#[cfg(feature = "text_layout")]
389pub use solver3::display_list::DisplayList as DisplayList3;
390#[cfg(feature = "text_layout")]
391pub use solver3::layout_document;
392#[cfg(feature = "text_layout")]
393pub use solver3::paged_layout::layout_document_paged;
394#[cfg(feature = "text_layout")]
395pub use solver3::{LayoutContext, LayoutError, Result as LayoutResult3};
396#[cfg(feature = "text_layout")]
397pub use text3::cache::{FontContext, FontManager, TextShapingCache};
398/// Backwards-compat alias for the old `TextLayoutCache` name.
399/// Will be dropped at the next API revision; new code should use
400/// [`TextShapingCache`] directly.
401#[cfg(feature = "text_layout")]
402pub use text3::cache::TextShapingCache as TextLayoutCache;
403#[cfg(feature = "font_async_registry")]
404pub use rust_fontconfig::registry::FcFontRegistry;
405#[cfg(feature = "text_layout")]
406pub use window::{CursorBlinkTimerAction, LayoutWindow, ScrollbarDragState, TooltipTimerAction};
407#[cfg(feature = "text_layout")]
408pub use managers::text_input::{PendingTextEdit, OptionPendingTextEdit};
409
410#[cfg(feature = "text_layout")]
411/// Parses raw font bytes into a [`FontRef`](azul_css::props::basic::FontRef)
412/// suitable for use in the layout system.
413// signature must match the `ParseFontFn = fn(LoadedFontSource) -> ...` callback type
414// (core/src/resources.rs) and the api.json export, so the owned param cannot become &.
415#[allow(clippy::needless_pass_by_value)]
416pub fn parse_font_fn(
417    source: azul_core::resources::LoadedFontSource,
418) -> Option<azul_css::props::basic::FontRef> {
419    use crate::font::parsed::ParsedFont;
420
421    ParsedFont::from_bytes(
422        source.data.as_ref(),
423        source.index as usize,
424        &mut Vec::new(), // Ignore warnings for now
425    )
426    .map(parsed_font_to_font_ref)
427}
428
429#[cfg(feature = "text_layout")]
430/// Wraps a [`ParsedFont`] in a [`FontRef`](azul_css::props::basic::FontRef),
431/// transferring ownership to the returned handle.
432pub fn parsed_font_to_font_ref(
433    parsed_font: ParsedFont,
434) -> azul_css::props::basic::FontRef {
435    use core::ffi::c_void;
436
437    extern "C" fn parsed_font_destructor(ptr: *mut c_void) {
438        unsafe {
439            drop(Box::from_raw(ptr.cast::<ParsedFont>()));
440        }
441    }
442
443    let boxed = Box::new(parsed_font);
444    let raw_ptr = Box::into_raw(boxed) as *const c_void;
445    azul_css::props::basic::FontRef::new(raw_ptr, parsed_font_destructor)
446}
447
448#[cfg(feature = "text_layout")]
449/// Recovers a reference to the [`ParsedFont`] stored inside a [`FontRef`](azul_css::props::basic::FontRef).
450///
451/// # Safety contract
452/// The `font_ref` must have been created by [`parsed_font_to_font_ref`],
453/// so that `font_ref.parsed` points to a valid `ParsedFont`.
454#[must_use] pub const fn font_ref_to_parsed_font(
455    font_ref: &azul_css::props::basic::FontRef,
456) -> &ParsedFont {
457    // SAFETY: `font_ref.parsed` was created by `parsed_font_to_font_ref`
458    // via `Box::into_raw`, so it points to a valid, aligned `ParsedFont`.
459    unsafe { &*font_ref.parsed.cast::<ParsedFont>() }
460}
461
462#[cfg(test)]
463mod autotest_generated {
464    //! Adversarial unit tests generated by the autotest fleet.
465    //!
466    //! Covers the four items defined directly in `lib.rs`:
467    //!   * `az_mark` / `az_mark_read` — the web-lift diagnostic markers. Only the
468    //!     `#[cfg(not(feature = "web_lift"))]` (no-op `const`) variants are
469    //!     exercised: the `web_lift` variants store to *absolute* addresses and
470    //!     would segfault a native test binary, so they are deliberately untested
471    //!     here (the doc comment says as much).
472    //!   * `parse_font_fn` — raw bytes → `Option<FontRef>`.
473    //!   * `parsed_font_to_font_ref` / `font_ref_to_parsed_font` — the
474    //!     `Box::into_raw` / reborrow round-trip, plus the refcounted
475    //!     clone/drop contract of the `FontRef` handle those two produce.
476
477    use super::*;
478
479    // ---------------------------------------------------------------
480    // az_mark / az_mark_read  (numeric — no-op variants)
481    // ---------------------------------------------------------------
482
483    /// Without `web_lift` the read is documented to return 0 for *every* address,
484    /// including the ends of the u32 range and the 0x40000–0xF0000 diagnostic band.
485    /// A non-zero answer here would mean the native build is really dereferencing
486    /// an absolute address.
487    #[cfg(not(feature = "web_lift"))]
488    #[test]
489    fn az_mark_read_is_zero_for_every_boundary_address() {
490        let addresses = [
491            0u32,
492            1,
493            0x3_FFFF,          // one below the diagnostic band
494            0x4_0000,          // band start
495            0x6_0758,          // a real marker counter from the docs
496            0xF_0000,          // band end
497            0xF_0001,          // one past the band
498            i32::MIN as u32,   // "negative" input, reinterpreted
499            (-1i32) as u32,    // == u32::MAX
500            u32::MAX - 1,
501            u32::MAX,
502            u32::MAX.wrapping_add(1), // wraps to 0, must not panic
503        ];
504        for addr in addresses {
505            assert_eq!(unsafe { az_mark_read(addr) }, 0, "az_mark_read(0x{addr:x})");
506        }
507    }
508
509    /// Sweep the whole u32 address space at a coarse stride: no address may panic
510    /// or return anything but 0.
511    #[cfg(not(feature = "web_lift"))]
512    #[test]
513    fn az_mark_read_sweeps_the_whole_address_space_as_zero() {
514        for addr in (0u32..=u32::MAX).step_by(1 << 24) {
515            assert_eq!(unsafe { az_mark_read(addr) }, 0);
516        }
517    }
518
519    /// A write must remain unobservable (the no-op variant stores nothing), for
520    /// every combination of boundary address and boundary value.
521    #[cfg(not(feature = "web_lift"))]
522    #[test]
523    fn az_mark_writes_are_unobservable_without_web_lift() {
524        let addresses = [0u32, 0x4_0000, 0x6_0758, 0xF_0000, u32::MAX];
525        let values = [0u32, 1, u32::MAX / 2, u32::MAX - 1, u32::MAX, i32::MIN as u32];
526        for addr in addresses {
527            for val in values {
528                unsafe { az_mark(addr, val) };
529                assert_eq!(
530                    unsafe { az_mark_read(addr) },
531                    0,
532                    "az_mark(0x{addr:x}, {val}) must not be observable"
533                );
534            }
535        }
536        // Repeating a write is still a no-op (idempotent, no accumulating state).
537        for _ in 0..1_000 {
538            unsafe { az_mark(0x6_0758, u32::MAX) };
539        }
540        assert_eq!(unsafe { az_mark_read(0x6_0758) }, 0);
541    }
542
543    /// Both no-op variants are `const fn`; this fails to *compile* if that ever
544    /// regresses (the `web_lift` variants are non-const on purpose, so this test
545    /// is gated off there).
546    #[cfg(not(feature = "web_lift"))]
547    #[test]
548    fn az_mark_no_op_variants_are_const_evaluable() {
549        const _WRITE_MIN: () = unsafe { az_mark(0, 0) };
550        const _WRITE_MAX: () = unsafe { az_mark(u32::MAX, u32::MAX) };
551        const READ_ZERO: u32 = unsafe { az_mark_read(0) };
552        const READ_MAX: u32 = unsafe { az_mark_read(u32::MAX) };
553        assert_eq!(READ_ZERO, 0);
554        assert_eq!(READ_MAX, 0);
555    }
556
557    // ---------------------------------------------------------------
558    // Shared font fixtures (text_layout only)
559    // ---------------------------------------------------------------
560
561    /// Positive control: the built-in `Azul Mock Mono` font (96 glyphs, upem 1000).
562    #[cfg(feature = "text_layout")]
563    const MOCK_MONO: &[u8] = crate::text3::mock_fonts::MOCK_MONO_TTF;
564
565    #[cfg(feature = "text_layout")]
566    fn loaded_source(bytes: Vec<u8>, index: u32) -> azul_core::resources::LoadedFontSource {
567        azul_core::resources::LoadedFontSource {
568            data: azul_css::U8Vec::from_vec(bytes),
569            index,
570            load_outlines: true,
571        }
572    }
573
574    #[cfg(feature = "text_layout")]
575    fn parse_mock() -> ParsedFont {
576        ParsedFont::from_bytes(MOCK_MONO, 0, &mut Vec::new())
577            .expect("Azul Mock Mono must parse (positive control)")
578    }
579
580    // ---------------------------------------------------------------
581    // parse_font_fn  (parser)
582    // ---------------------------------------------------------------
583
584    /// Malformed / hostile byte soup must come back as `None`, never a panic and
585    /// never a bogus `FontRef` (which would later be dereferenced as a `ParsedFont`).
586    #[cfg(feature = "text_layout")]
587    #[test]
588    fn parse_font_fn_rejects_malformed_input() {
589        let cases: Vec<(&str, Vec<u8>)> = vec![
590            ("empty", Vec::new()),
591            ("single_nul", vec![0u8]),
592            ("whitespace_only", b"   \t\n".to_vec()),
593            ("garbage", (0u8..=255).cycle().take(4096).collect()),
594            ("invalid_utf8", vec![0xFF, 0xFE, 0x00]),
595            ("sfnt_magic_only", vec![0x00, 0x01, 0x00, 0x00]),
596            ("header_only", MOCK_MONO[..12].to_vec()),
597            ("truncated_font", MOCK_MONO[..64].to_vec()),
598            ("half_a_font", MOCK_MONO[..MOCK_MONO.len() / 2].to_vec()),
599            ("unicode_emoji", "\u{1F600}\u{1F600}".repeat(1_000).into_bytes()),
600            ("combining_marks", "e\u{0301}".repeat(10_000).into_bytes()),
601            ("nested_brackets", vec![b'['; 10_000]),
602            ("boundary_numbers", b"0 -0 9223372036854775807 NaN inf -inf 1e309".to_vec()),
603            ("leading_junk_then_font", {
604                let mut v = b"garbage".to_vec();
605                v.extend_from_slice(MOCK_MONO);
606                v
607            }),
608        ];
609        for (name, bytes) in cases {
610            assert!(
611                parse_font_fn(loaded_source(bytes, 0)).is_none(),
612                "{name} must not parse into a FontRef"
613            );
614        }
615    }
616
617    /// Multi-megabyte junk: must terminate quickly and return `None`, not hang or
618    /// allocate its way out of memory (the sfnt table directory is attacker-controlled).
619    #[cfg(feature = "text_layout")]
620    #[test]
621    fn parse_font_fn_survives_extremely_long_input() {
622        assert!(parse_font_fn(loaded_source(vec![0u8; 1_000_000], 0)).is_none());
623        assert!(parse_font_fn(loaded_source(vec![b'a'; 1_000_000], 0)).is_none());
624        // "ttcf" collection magic followed by a megabyte of junk offsets.
625        let mut ttcf = b"ttcf".to_vec();
626        ttcf.extend_from_slice(&vec![0xABu8; 1_000_000]);
627        assert!(parse_font_fn(loaded_source(ttcf, 0)).is_none());
628    }
629
630    /// Positive control: a real font parses, and the handle we get back really does
631    /// point at the parsed face (this is the only sanctioned way to build the
632    /// `FontRef` that `font_ref_to_parsed_font` is allowed to reborrow).
633    #[cfg(feature = "text_layout")]
634    #[test]
635    fn parse_font_fn_parses_the_positive_control() {
636        let font_ref = parse_font_fn(loaded_source(MOCK_MONO.to_vec(), 0))
637            .expect("the positive control must parse");
638        let parsed = font_ref_to_parsed_font(&font_ref);
639
640        assert_eq!(parsed.num_glyphs(), 96);
641        assert_eq!(parsed.num_glyphs(), parsed.maxp_table.num_glyphs);
642        assert_eq!(parsed.font_metrics.units_per_em, 1000);
643        assert!(parsed.font_metrics.ascent > 0.0);
644        assert!(parsed.font_metrics.descent <= 0.0);
645        assert!(parsed.font_metrics.ascent.is_finite());
646        assert!(parsed.font_metrics.descent.is_finite());
647        assert!(parsed.font_metrics.line_gap.is_finite());
648        assert_eq!(parsed.font_type, FontType::TrueType);
649        assert_eq!(parsed.original_index, 0);
650        assert!(parsed.cmap_subtable.is_some());
651        assert_eq!(parsed.hash, parse_mock().hash);
652    }
653
654    /// `load_outlines` is not consulted by `parse_font_fn` (only `data` + `index`
655    /// are). Both settings must therefore yield the same face — if this ever
656    /// diverges, callers that flip the flag silently get a different font.
657    #[cfg(feature = "text_layout")]
658    #[test]
659    fn parse_font_fn_ignores_the_load_outlines_flag() {
660        let with = azul_core::resources::LoadedFontSource {
661            data: azul_css::U8Vec::from_vec(MOCK_MONO.to_vec()),
662            index: 0,
663            load_outlines: true,
664        };
665        let without = azul_core::resources::LoadedFontSource {
666            data: azul_css::U8Vec::from_vec(MOCK_MONO.to_vec()),
667            index: 0,
668            load_outlines: false,
669        };
670        let a = parse_font_fn(with).expect("parses with outlines");
671        let b = parse_font_fn(without).expect("parses without outlines");
672        let (pa, pb) = (font_ref_to_parsed_font(&a), font_ref_to_parsed_font(&b));
673        assert_eq!(pa.hash, pb.hash);
674        assert_eq!(pa.num_glyphs(), pb.num_glyphs());
675        assert_eq!(pa.pdf_font_metrics, pb.pdf_font_metrics);
676    }
677
678    /// `index` is cast `u32 as usize` and fed to the table provider. Out-of-range
679    /// face indices on a single-face font must be deterministic — no panic, no
680    /// `12 + index * 4` overflow, and no face with a different glyph count.
681    #[cfg(feature = "text_layout")]
682    #[test]
683    fn parse_font_fn_with_an_out_of_range_index_is_deterministic() {
684        let baseline = parse_mock();
685        for index in [1u32, 2, 0x7FFF_FFFF, u32::MAX - 1, u32::MAX] {
686            if let Some(font_ref) = parse_font_fn(loaded_source(MOCK_MONO.to_vec(), index)) {
687                let parsed = font_ref_to_parsed_font(&font_ref);
688                assert_eq!(
689                    parsed.num_glyphs(),
690                    baseline.num_glyphs(),
691                    "index {index} must not conjure a different face"
692                );
693                assert_eq!(parsed.original_index, index as usize);
694            }
695        }
696    }
697
698    /// Empty / garbage input on the failing path must not leak or corrupt state
699    /// across repeated calls (the destructor is only installed on the `Some` path).
700    #[cfg(feature = "text_layout")]
701    #[test]
702    fn parse_font_fn_failure_path_is_repeatable() {
703        for _ in 0..200 {
704            assert!(parse_font_fn(loaded_source(Vec::new(), 0)).is_none());
705            assert!(parse_font_fn(loaded_source(vec![0xFF; 3], u32::MAX)).is_none());
706        }
707        // …and a good parse still works afterwards.
708        assert!(parse_font_fn(loaded_source(MOCK_MONO.to_vec(), 0)).is_some());
709    }
710
711    /// Every successful parse mints a *fresh* identity, even for byte-identical
712    /// input: `FontRef` equality is the never-reused `id`, not the heap pointer
713    /// (freeing a font and reusing its address must not forge identity).
714    #[cfg(feature = "text_layout")]
715    #[test]
716    fn parse_font_fn_mints_a_fresh_identity_per_call() {
717        let a = parse_font_fn(loaded_source(MOCK_MONO.to_vec(), 0)).expect("parses");
718        let b = parse_font_fn(loaded_source(MOCK_MONO.to_vec(), 0)).expect("parses");
719        assert_ne!(a, b, "two parses of the same bytes are two distinct handles");
720        assert_ne!(a.id, b.id);
721        assert!(b > a, "ids are monotonically assigned");
722        // …but the *content* is identical.
723        assert_eq!(
724            font_ref_to_parsed_font(&a).hash,
725            font_ref_to_parsed_font(&b).hash
726        );
727    }
728
729    // ---------------------------------------------------------------
730    // parsed_font_to_font_ref / font_ref_to_parsed_font  (round-trip)
731    // ---------------------------------------------------------------
732
733    /// encode == decode: wrapping a `ParsedFont` and reborrowing it must hand back
734    /// the very same face, field for field.
735    #[cfg(feature = "text_layout")]
736    #[test]
737    fn parsed_font_font_ref_round_trip_preserves_the_face() {
738        let original = parse_mock();
739        let expected_hash = original.hash;
740        let expected_glyphs = original.num_glyphs();
741        let expected_metrics = original.pdf_font_metrics;
742        let expected_upem = original.font_metrics.units_per_em;
743        let expected_ascent = original.font_metrics.ascent;
744        let expected_type = original.font_type.clone();
745        let expected_index = original.original_index;
746
747        let font_ref = parsed_font_to_font_ref(original);
748        let decoded = font_ref_to_parsed_font(&font_ref);
749
750        assert_eq!(decoded.hash, expected_hash);
751        assert_eq!(decoded.num_glyphs(), expected_glyphs);
752        assert_eq!(decoded.pdf_font_metrics, expected_metrics);
753        assert_eq!(decoded.font_metrics.units_per_em, expected_upem);
754        assert!((decoded.font_metrics.ascent - expected_ascent).abs() < f32::EPSILON);
755        assert_eq!(decoded.font_type, expected_type);
756        assert_eq!(decoded.original_index, expected_index);
757    }
758
759    /// The freshly-minted handle's invariants: live pointer, refcount of exactly 1,
760    /// destructor armed, non-zero id (id 0 flags a raw-reconstructed handle).
761    #[cfg(feature = "text_layout")]
762    #[test]
763    fn parsed_font_to_font_ref_handle_invariants() {
764        use core::sync::atomic::Ordering as AtomicOrdering;
765
766        let font_ref = parsed_font_to_font_ref(parse_mock());
767        assert!(!font_ref.parsed.is_null());
768        assert!(!font_ref.copies.is_null());
769        assert!(font_ref.run_destructor);
770        assert_ne!(font_ref.id, 0, "id 0 is reserved for un-initialised handles");
771        assert_eq!(unsafe { (*font_ref.copies).load(AtomicOrdering::SeqCst) }, 1);
772        assert_eq!(font_ref.get_parsed(), font_ref.parsed);
773    }
774
775    /// `font_ref_to_parsed_font` is a pure reborrow: repeated calls must yield the
776    /// same address, and that address must be the handle's `parsed` pointer.
777    #[cfg(feature = "text_layout")]
778    #[test]
779    fn font_ref_to_parsed_font_is_a_stable_reborrow() {
780        use core::ffi::c_void;
781
782        let font_ref = parsed_font_to_font_ref(parse_mock());
783        let first: *const ParsedFont = font_ref_to_parsed_font(&font_ref);
784        let second: *const ParsedFont = font_ref_to_parsed_font(&font_ref);
785        assert!(core::ptr::eq(first, second), "reborrow must be stable");
786        assert!(core::ptr::eq(first.cast::<c_void>(), font_ref.get_parsed()));
787    }
788
789    /// A clone shares the face (same pointer, same id) and bumps the refcount;
790    /// dropping the clone must NOT free the face out from under the original.
791    /// Reading through the survivor after the drop is the use-after-free probe.
792    #[cfg(feature = "text_layout")]
793    #[test]
794    fn cloning_a_font_ref_shares_the_face_and_the_drop_is_refcounted() {
795        use core::sync::atomic::Ordering as AtomicOrdering;
796
797        let original = parsed_font_to_font_ref(parse_mock());
798        let expected_hash = font_ref_to_parsed_font(&original).hash;
799
800        let clone = original.clone();
801        assert_eq!(clone.id, original.id, "a clone is the same font");
802        assert_eq!(clone, original);
803        assert!(core::ptr::eq(clone.parsed, original.parsed));
804        assert_eq!(unsafe { (*original.copies).load(AtomicOrdering::SeqCst) }, 2);
805
806        drop(clone);
807        assert_eq!(unsafe { (*original.copies).load(AtomicOrdering::SeqCst) }, 1);
808        assert_eq!(
809            font_ref_to_parsed_font(&original).hash,
810            expected_hash,
811            "the face must survive its clone being dropped"
812        );
813        assert_eq!(font_ref_to_parsed_font(&original).num_glyphs(), 96);
814    }
815
816    /// Hammer the refcount: 1_000 clone/drop cycles (plus a batch held live at once)
817    /// must leave the face readable and the count back at 1 — a double-decrement
818    /// would free the `ParsedFont` early and turn the next reborrow into a UAF.
819    #[cfg(feature = "text_layout")]
820    #[test]
821    fn font_ref_clone_drop_cycles_do_not_double_free() {
822        use core::sync::atomic::Ordering as AtomicOrdering;
823
824        let original = parsed_font_to_font_ref(parse_mock());
825        let expected_hash = font_ref_to_parsed_font(&original).hash;
826
827        for _ in 0..1_000 {
828            let c = original.clone();
829            assert_eq!(font_ref_to_parsed_font(&c).hash, expected_hash);
830        }
831
832        let batch: Vec<_> = (0..1_000).map(|_| original.clone()).collect();
833        assert_eq!(
834            unsafe { (*original.copies).load(AtomicOrdering::SeqCst) },
835            1_001
836        );
837        drop(batch);
838        assert_eq!(unsafe { (*original.copies).load(AtomicOrdering::SeqCst) }, 1);
839        assert_eq!(font_ref_to_parsed_font(&original).hash, expected_hash);
840    }
841
842    /// Identity semantics as a hash/ordering key: clones collapse, independently
843    /// wrapped faces don't — even when they hold byte-identical font data.
844    #[cfg(feature = "text_layout")]
845    #[test]
846    fn font_ref_identity_is_per_handle_not_per_content() {
847        use std::collections::{BTreeSet, HashSet};
848
849        let a = parsed_font_to_font_ref(parse_mock());
850        let b = parsed_font_to_font_ref(parse_mock());
851        assert_ne!(a, b);
852        assert!(a < b, "ids are monotonically assigned, so a precedes b");
853        assert_eq!(
854            font_ref_to_parsed_font(&a).hash,
855            font_ref_to_parsed_font(&b).hash,
856            "…even though the content hash is the same"
857        );
858
859        let set: HashSet<_> = vec![a.clone(), a.clone(), a.clone(), b.clone()]
860            .into_iter()
861            .collect();
862        assert_eq!(set.len(), 2, "clones dedup, distinct handles do not");
863
864        let ordered: BTreeSet<_> = vec![b.clone(), a.clone(), b.clone()].into_iter().collect();
865        assert_eq!(ordered.len(), 2);
866        assert_eq!(ordered.iter().next(), Some(&a));
867    }
868
869    /// Wrapping many faces in a row must keep every handle pointing at its *own*
870    /// face — a shared/stale `Box::into_raw` would make them alias.
871    #[cfg(feature = "text_layout")]
872    #[test]
873    fn many_font_refs_do_not_alias_each_other() {
874        let refs: Vec<_> = (0..16).map(|_| parsed_font_to_font_ref(parse_mock())).collect();
875        for (i, a) in refs.iter().enumerate() {
876            assert_eq!(font_ref_to_parsed_font(a).num_glyphs(), 96);
877            for b in refs.iter().skip(i + 1) {
878                assert!(
879                    !core::ptr::eq(a.parsed, b.parsed),
880                    "independently boxed faces must not share a pointer"
881                );
882                assert_ne!(a.id, b.id);
883            }
884        }
885    }
886}