Skip to content

text-1a: the textlayout resolver at oracle v0 - #70

Merged
softmarshmallow merged 2 commits into
mainfrom
text-1-textlayout
Aug 4, 2026
Merged

text-1a: the textlayout resolver at oracle v0#70
softmarshmallow merged 2 commits into
mainfrom
text-1-textlayout

Conversation

@softmarshmallow

Copy link
Copy Markdown
Member

The first code rung of the text arc (gridaco/nothing#69), executing against the method ratified in #68. It builds the second real text producer — the thing D-M's shaped-text stage has been gated on since it was recorded.

What this is

crates/textlayout — the Web family's text resolution oracle, implementing the text-layout RFD at its smallest honest profile:

attributed text + explicit font environment -> resolved text layout | typed refusal   (oracle v0)

Why an artifact, not a shaping helper

The obvious build — shape, flatten to outlines, done — lands correct pixels and unlocks nothing. D-M asks whether a neutral shaped-text representation and font-key boundary can be produced by both engines; outlines are geometry that deliberately carries no font identity at all. A producer that discards its shaped-text artifact is not a second producer in the D-M sense, and the discovery would come at text-2 with nothing to push against n0's artifact.

So the artifact exists and is inspectable inside the producer, even though it never crosses rframe. Outline lowering is its first consumer.

Equally deliberate: this is the Web family's producer, not an engine-wide text service. Building it as the anointed shared resolver would decide D-M high by construction and make text-2 theater. The dependency direction is websem -> textlayout only; join-low stays a live outcome.

The profile, and what it refuses

v0 resolves one style run of printable-ASCII, horizontal, LTR text — no wrapping, fallback, or synthesis. The repertoire is an explicit admit-list enforced by the resolver, so out-of-profile input refuses by byte position regardless of what any font happens to cover. Refusals: unknown family, unparseable face, color/bitmap face, out-of-profile character, missing glyph, out-of-profile shaping (offsets, vertical advance, negative advance), invalid size.

Identity locked by architecture tests: perimeter exactly {rustybuzz} across every dependency-table shape plus no build script; no ambient font database, render contract, backend, clock, or I/O in src.

Verified adversarially

Three reviewers with distinct lenses (RFD conformance, correctness, repo laws) were prompted to refute the crate, and did — against fonts outside the fixture corpus. Every must-fix is folded in:

  • The profile was enforced by Ahem's coverage, not by the resolver. Hebrew resolved un-reordered against a covering font; U+202E RLO passed the is_control() guard (Cf, not Cc) and entered the artifact as a phantom space glyph; U+2028 evaded the line-terminator claim; ZWSP/soft-hyphen were silently substituted by the shaper's hide-default-ignorables pass. The admit-list closed all six at once.
  • y_advance and negative advances were dropped, flattening GPOS vertical pen movement onto the baseline.
  • Color/bitmap faces resolved to monochrome placeholder outlines — demonstrated with Apple Color Emoji. Now refused whole by raw table tag.
  • outline() accepted any caller-built glyph, so a glyph id from another font could be streamed through this artifact's face. Now index-based over recorded placements.
  • Glyph-id panic → typed refusal; rustybuzz pinned =0.20.1 so ORACLE_VERSION's stability claim is mechanical; both architecture locks hardened; FontKey's doc no longer claims a verification it does not perform.

Declined with reasons recorded: in-crate hashing (a sha2 perimeter edge is an owner decision; the brief assigns verification to the host, gated when n0_cli --font lands), a MissingGlyph test (unreachable through Ahem over ASCII — an honest comment beats a fake pin), line-gap in LineMetrics (declared deferral).

Ground truth

Tests assert the numbers measured in the text-0 probe rounds and the crux spike — glyph 58, 1000-unit advances, the 0.8/0.2 em split, the y-flip vertices — not values derived from this crate. Lock delta is pure addition.

Also promotes the text-oracle brief to ratified now that #68 has merged.

Next in the arc: the websem <text> arm consuming this crate, then the text cells and svg-text graduation.

The producer-side implementation of the single text-resolution contract
(docs/wg/feat-paragraph/text-layout.md) at its smallest honest profile:
one style run of horizontal LTR text, no wrapping, no fallback, no
synthesis — everything outside the profile is a typed refusal, and
coverage grows by oracle version as the RFD prescribes.

- hermetic environment: a manifest of caller-verified font bytes
  (FontKey = host-checked content digest); empty by default, so
  undeclared text refuses instead of reaching for a system font
- the immutable artifact: resolved face identity, positioned glyphs
  with cluster mappings, fractional advances, line metrics, logical
  and ink bounds — consumers project, they never re-resolve
- the y-flip owned once: outlines stream through the crate's own sink
  in y-down local px, pinned by test against the measured Ahem box
- identity locked by architecture tests: dependency perimeter exactly
  {rustybuzz}; no fontdb, no render contract, no backend, no clock,
  no I/O in src

This is the Web family's producer, not an engine-wide text service —
the D-M shaped-text join (docs/wg/consolidation/n0-join-point.md)
stays open, and the dependency direction is websem -> textlayout only.
Unit tests assert the ground truth measured in the text-0 probe rounds
and crux spike. Lock delta is pure addition (rustybuzz already in the
graph via vendored usvg).
Merged as #68. The gate ladder, admitted numeric domain,
bake posture, hermetic font environment, corpus-growth law, outlines-first
lowering, and the resolver crate now bind the text arc's rungs. The D-M
shaped-text stage stays open — text-2 is the spike that closes it.
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nothing Ready Ready Preview Aug 4, 2026 10:52am

Request Review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The PR adds the textlayout workspace crate. It resolves one attributed printable-ASCII run using declared font bytes, produces immutable geometry and outlines, returns typed refusals, adds architecture and oracle tests, and ratifies the text oracle documentation.

Changes

Text layout oracle

Layer / File(s) Summary
Crate contract and workspace integration
Cargo.toml, crates/textlayout/Cargo.toml, crates/textlayout/src/lib.rs, AGENTS.md
The workspace registers the internal textlayout crate. Its oracle-v0 scope, public API, and crate checks are documented.
Font environment and layout artifact
crates/textlayout/src/environment.rs, crates/textlayout/src/artifact.rs
The crate adds SHA-256 font identities, explicit font resources, immutable layout data, geometry accessors, and streamed glyph outlines with y-axis conversion.
Deterministic resolution pipeline
crates/textlayout/src/resolve.rs
The resolver validates input and font capabilities, shapes text with rustybuzz, computes metrics and ink bounds, and returns typed errors or a resolved layout.
Architecture validation and oracle ratification
crates/textlayout/tests/architecture.rs, crates/textlayout/tests/oracle_v0.rs, docs/wg/consolidation/index.md, docs/wg/consolidation/text-oracle.md
Tests enforce dependency and source-access boundaries and verify Ahem-based oracle behavior. Consolidation documents mark the text oracle as ratified.
Estimated code review effort: 4 (Complex) ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant resolve
  participant Environment
  participant rustybuzz
  participant ResolvedTextLayout
  Caller->>resolve: submit AttributedText and Environment
  resolve->>Environment: find declared family
  Environment-->>resolve: return FontResource
  resolve->>rustybuzz: shape printable-ASCII text
  rustybuzz-->>resolve: return glyph placements
  resolve->>ResolvedTextLayout: store metrics, bounds, glyphs, and font bytes
  ResolvedTextLayout-->>Caller: return immutable layout
Loading

Possibly related issues

  • gridaco/nothing issue 69 — Directly covers the text-1 backend-free resolver crate and oracle-v0 contract implemented here.

Possibly related PRs

  • gridaco/nothing#26 — Defines the universal shaped text layout contract implemented by this PR.
  • gridaco/nothing#68 — Proposes and ratifies the resolver architecture and Ahem-based oracle contract implemented here.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the textlayout resolver and its oracle v0 scope.
Description check ✅ Passed The description directly explains the textlayout resolver, its profile, typed refusals, tests, and ratification changes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch text-1-textlayout

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
crates/textlayout/src/resolve.rs (1)

224-230: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider accumulating the pen in font units.

pen_x accumulates already-scaled f32 values. Each addition adds rounding error, so a long run drifts from the exact scaled sum of integer advances. Accumulating the advance in font units and scaling at use keeps every recorded x and the total advance exact to one rounding step.

♻️ Proposed refactor
     let mut glyphs = Vec::with_capacity(shaped.len());
-    let mut pen_x = 0.0f32;
+    let mut pen_units: i64 = 0;
@@
         glyphs.push(PlacedGlyph {
             glyph_id,
-            x: pen_x,
+            x: pen_units as f32 * scale,
             advance: pos.x_advance as f32 * scale,
             cluster: info.cluster,
         });
-        pen_x += pos.x_advance as f32 * scale;
+        pen_units += i64::from(pos.x_advance);
     }
+    let pen_x = pen_units as f32 * scale;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/textlayout/src/resolve.rs` around lines 224 - 230, Update the pen
position handling around glyph placement so `pen_x` accumulates unscaled
font-unit advances, and apply `scale` only when assigning each glyph’s `x` and
computing the final total. Preserve integer advance accumulation and ensure
every recorded position is converted to `f32` once after scaling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/textlayout/src/resolve.rs`:
- Around line 163-173: The face validation around NON_OUTLINE_GLYPH_TABLES must
reject faces without an outline source before resolution proceeds. Add a
positive requirement for at least one of glyf, CFF , or CFF2, and extend the
unsupported-table checks to include monochrome bitmap tables EBDT and EBLC while
preserving the existing color/bitmap refusals.
- Line 186: Update the descent calculation to convert face.descender() to f32
before applying negation, avoiding i16::MIN overflow while preserving the
existing scaling behavior.
- Around line 177-181: Update the units_per_em handling in the face-resolution
flow to validate the original face.units_per_em() value, rejecting zero and any
value outside 16..=16384 with ResolveError::UnparseableFace before converting or
dividing. Preserve the existing family context and only compute scale after
validation succeeds.

In `@crates/textlayout/tests/architecture.rs`:
- Around line 55-81: Extend the FORBIDDEN denylist in the architecture test to
reject the compile-time ambient-input macros include_bytes!, include_str!, env!,
and option_env!. Keep the existing path-based checks and add entries that catch
these macro usages so undeclared files or environment values cannot bypass the
resolver restrictions.

---

Nitpick comments:
In `@crates/textlayout/src/resolve.rs`:
- Around line 224-230: Update the pen position handling around glyph placement
so `pen_x` accumulates unscaled font-unit advances, and apply `scale` only when
assigning each glyph’s `x` and computing the final total. Preserve integer
advance accumulation and ensure every recorded position is converted to `f32`
once after scaling.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2e6316ab-9bc4-4ef0-a8b4-22dfdb7b3008

📥 Commits

Reviewing files that changed from the base of the PR and between e4f8b3b and e872787.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • AGENTS.md
  • Cargo.toml
  • crates/textlayout/Cargo.toml
  • crates/textlayout/src/artifact.rs
  • crates/textlayout/src/environment.rs
  • crates/textlayout/src/lib.rs
  • crates/textlayout/src/resolve.rs
  • crates/textlayout/tests/architecture.rs
  • crates/textlayout/tests/oracle_v0.rs
  • docs/wg/consolidation/index.md
  • docs/wg/consolidation/text-oracle.md

Comment on lines +163 to +173
for tag in NON_OUTLINE_GLYPH_TABLES {
if face
.raw_face()
.table(rustybuzz::ttf_parser::Tag::from_bytes(tag))
.is_some()
{
return Err(ResolveError::UnsupportedFaceFormat {
family: text.style.family.clone(),
});
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether the oracle tests pin behavior for outline-less or bitmap-only faces.
set -euo pipefail

fd -t f -e rs . crates/textlayout --exec rg -n -C4 'UnsupportedFaceFormat|EBDT|EBLC|CBDT|glyf|CFF' {}

Repository: gridaco/nothing

Length of output: 2401


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file size =="
wc -l crates/textlayout/src/resolve.rs

echo "== resolve outline =="
ast-grep outline crates/textlayout/src/resolve.rs --view expanded || true

echo "== resolve relevant section =="
sed -n '1,260p' crates/textlayout/src/resolve.rs

echo "== search textlayout outline behavior =="
rg -n -C3 'glyph_bounding_box|outline|ResolvedTextLayout|ink_union|NON_OUTLINE_GLYPH_TABLES|resolve\(' crates/textlayout/src crates/textlayout/tests

Repository: gridaco/nothing

Length of output: 27748


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== tests with UnsupportedFaceFormat =="
fd -t f -e rs . crates/textlayout --exec rg -n -C5 'UnsupportedFaceFormat|color or bitmap|outline glyph|outlineGlyph|outline' {} || true

echo "== oracle fixtures list =="
git ls-files crates/assets fixtures crates/textlayout | sed -n '1,200p'

echo "== inspect test files sizes =="
for f in crates/textlayout/tests/*.rs; do
  echo "--- $f ($(wc -l < "$f") lines) ---"
  sed -n '1,260p' "$f"
done

Repository: gridaco/nothing

Length of output: 28285


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== textlayout Cargo.toml =="
cat crates/textlayout/Cargo.toml

echo "== search web-first ahem =="
git ls-files | rg '(^fixtures/web-first/fonts/ahem\.ttf$|fixtures/web-first/)' | sed -n '1,200p'

echo "== find NotoColorEmoji =="
fd -a 'NotoColorEmoji-Regular\.ttf$|unifont.*\.otf$' . | sed -n '1,80p'

python3 - <<'PY'
from pathlib import Path
for p in Path('.').rglob('*.ttf'):
    s = p.read_bytes()[:20]
    if b'COLR' in s or b'CBDT' in s or b'CBLC' in s or b'EBDT' in s or b'EBLC' in s or b'glyf' in s or b'CFF ' in s or b'CFF2' in s or s.startswith((b'ttcf',)):
        print(p, s.hex())
for p in Path('.').rglob('*.otf'):
    s = p.read_bytes()[:20]
    if b'COLR' in s or b'CBDT' in s or b'CBLC' in s or b'EBDT' in s or b'EBLC' in s or b'glyf' in s or b'CFF ' in s or b'CFF2' in s or s.startswith((b'ttcf',)):
        print(p, s.hex())
PY

Repository: gridaco/nothing

Length of output: 13793


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== textlayout Cargo.toml =="
cat crates/textlayout/Cargo.toml

echo "== repository tracked fonts likely bitmap/color =="
python3 - <<'PY'
from pathlib import Path
def scan(path):
    try:
        data = path.read_bytes()[:2000]
    except Exception as e:
        print(path, "ERR", e)
        return
    tags = []
    for tag in [b'COLR', b'CBDT', b'CBLC', b'EBDT', b'EBLC', b'glyf', b' C  ', b' C V ', b'CFF ', b'CFF2', b'sbgx', b'sbix']:
        if tag in data:
            tags.append(tag.decode('latin1', 'replace'))
    if tags or data[:4] in [b'ttcf']:
        print(path, data[:4].decode(errors='replace'), tags)

for ext in ('.ttf', '.otf', '.ttc'):
    for p in Path('.').rglob('*' + ext):
        if p.parts[:3] == ('fixtures', 'web-first', 'fonts'):
            scan(p)
PY

echo "== fixture index mentions for textlayout fonts =="
rg -n 'Ahem|unifont|Noto Color|ColorEmoji|unifont|otf|ttf' fixtures/textlayout crates/textlayout/fixtures fixtures | sed -n '1,200p' || true

Repository: gridaco/nothing

Length of output: 12202


Refuse faces that have no outline source.

The current guard only rejects color/bitmap tables such as COLR, CBDT, CBLC, sbix, and SVG . It allows faces that parse but have no outline source (glyf, CFF , CFF2) and allows monochrome bitmap tables such as EBDT/EBLC. Resolution then succeeds with advance-only glyphs, and both ink_bounds() and outline() treat them as if the glyph has no pixel, like the space glyph. Add a positive outline-source requirement and include the applicable bitmap tables in the refusal list.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/textlayout/src/resolve.rs` around lines 163 - 173, The face validation
around NON_OUTLINE_GLYPH_TABLES must reject faces without an outline source
before resolution proceeds. Add a positive requirement for at least one of glyf,
CFF , or CFF2, and extend the unsupported-table checks to include monochrome
bitmap tables EBDT and EBLC while preserving the existing color/bitmap refusals.

Source: Coding guidelines

Comment on lines +177 to +181
let units_per_em =
u16::try_from(face.units_per_em()).map_err(|_| ResolveError::UnparseableFace {
family: text.style.family.clone(),
})?;
let scale = size / f32::from(units_per_em);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

rustybuzz 0.20.1 Face units_per_em return type ttf-parser valid range

💡 Result:

In rustybuzz 0.20.1, the Face::units_per_em method typically returns a u16 value [1][2]. While some internal representations or wrapper methods may cast this to an i32 (for instance, to match HarfBuzz C API expectations) [3], the underlying data type provided by the ttf-parser dependency is a u16 [4][5]. Regarding the valid range, the units_per_em value is guaranteed to be within the range 16..=16384 [1][2][4][5]. This range is enforced by the ttf-parser library, which provides the font parsing logic for rustybuzz [4][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate relevant files =="
fd -a 'resolve\.rs|Cargo\.toml|Cargo\.lock' . | sed 's#^\./##' | head -100

echo
echo "== dependency versions =="
rg -n 'rustybuzz|ttf-parser|ttfautohint' Cargo.toml Cargo.lock ./crates/textlayout -S 2>/dev/null | head -200 || true

echo
echo "== resolve.rs outline and relevant sections =="
if [ -f crates/textlayout/src/resolve.rs ]; then
  wc -l crates/textlayout/src/resolve.rs
  ast-grep outline crates/textlayout/src/resolve.rs 2>/dev/null || true
  sed -n '140,195p' crates/textlayout/src/resolve.rs
fi

Repository: gridaco/nothing

Length of output: 6111


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== cargo metadata for rustybuzz 0.20.1 =="
cargo metadata --format-version 1 2>/tmp/cargo-meta.stderr | jq -r '
  .packages[] | select(.name == "rustybuzz" and .version == "0.20.1") | {
    name, version, source, manifest_path, deps: (.dependencies // []) | map({name, version, optional})
  }
' || {
  cat /tmp/cargo-meta.stderr
  exit 1
}

echo
echo "== check local rustybuzz API source if present =="
python3 - <<'PY'
from pathlib import Path
for p in Path('.rg','/cargo/registry/src').glob('**/rustybuzz-0.20.1/src/**/*.rs'):
    if 'units_per_em' in p.read_text(errors='replace'):
        text = p.read_text(errors='replace')
        print(f'FILE {p}')
        for i,line in enumerate(text.splitlines(),1):
            if 'units_per_em' in line or 'pub fn units_per_em' in line:
                print(f'{i}: {line}')
PY

echo
echo "== inspect rustybuzz Face units_per_em references in cargo source cache =="
python3 - <<'PY'
from pathlib import Path
roots = [Path.home()/'.cargo/registry/src', Path('/usr/local/cargo/registry/src')]
hits=[]
for root in roots:
    if root.exists():
        for p in root.rglob('*.rs'):
            if 'units_per_em' in p.read_text(errors='ignore'):
                for line in p.read_text(errors='ignore').splitlines():
                    if 'units_per_em' in line:
                        hits.append((p,line.strip()))
for p,line in hits[:50]:
    print(f'{p}: {line}')
PY

echo
echo "== local API references =="
rg -n 'fn units_per_em|units_per_em\(\)' --glob '*.rs' . | head -120

Repository: gridaco/nothing

Length of output: 145


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== check if Cargo.lock contains exact rustybuzz version and source =="
rg -n '^name = "rustybuzz"$|rustybuzz = "=0.20.1"|checksum ="|source = ' Cargo.lock -A8 -B2 | sed -n '1,220p'

echo
echo "== search local .cargo if available =="
if [ -d "$HOME/.cargo/registry/src" ]; then
  rg -n 'pub fn units_per_em|fn units_per_em|units_per_em\(&self\)' "$HOME/.cargo/registry/src" -g '*rustybuzz*' 2>/dev/null | head -80 || true
else
  echo "NO $HOME/.cargo/registry/src"
fi

echo
echo "== exact local units_per_em usages =="
rg -n 'units_per_em|face\.units_per_em|ttf_parser::.*units_per_em' --glob '*.rs' . | head -120

Repository: gridaco/nothing

Length of output: 9535


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== inspect artifact.rs units_per_em path =="
sed -n '1,45p' crates/textlayout/src/artifact.rs
sed -n '185,210p' crates/textlayout/src/artifact.rs
sed -n '225,245p' crates/textlayout/src/artifact.rs

echo
echo "== inspect resolve error handling =="
sed -n '25,90p' crates/textlayout/src/resolve.rs
sed -n '166,184p' crates/textlayout/src/resolve.rs

echo
echo "== inspect usvg ttf-parser units_per_em assumptions =="
sed -n '1190,1250p' third_party/usvg/src/text/layout.rs

Repository: gridaco/nothing

Length of output: 10104


🌐 Web query:

rustybuzz 0.20.1 Face units_per_em i32 source units_per_em

💡 Result:

In rustybuzz version 0.20.1, the units_per_em method on the Face (or hb_font_t) structure returns an i32 [1][2]. The underlying implementation stores the value as a u16 field within the struct [1][2]. The units_per_em method acts as a wrapper that casts this internal u16 value to an i32 [1][3]: #[inline] pub fn units_per_em(&self) -> i32 { self.units_per_em as i32 } This casting is provided to maintain API compatibility or convenience, while the actual font data (guaranteed to be in the range 16..=16384) is held in the u16 storage field [4][5][1].

Citations:


Reject malformed units_per_em instead of treating parser output as dead.

units_per_em is read from face.units_per_em() then stored as u16, so an out-of-range report through that API path can still exist if the parser is not the bounded units_per_em(...) API. Reject the face when the value is outside 16..=16384 or is zero to prevent division by zero and non-finite geometry scaling.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/textlayout/src/resolve.rs` around lines 177 - 181, Update the
units_per_em handling in the face-resolution flow to validate the original
face.units_per_em() value, rejecting zero and any value outside 16..=16384 with
ResolveError::UnparseableFace before converting or dividing. Preserve the
existing family context and only compute scale after validation succeeds.

ascent: f32::from(face.ascender()) * scale,
// ttf-parser reports descent as a negative distance; the artifact
// states it as a positive reach below the baseline.
descent: f32::from(-face.descender()) * scale,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Widen before negating the descender.

face.descender() returns i16. -i16::MIN overflows, which panics in a debug build and wraps in a release build. Widen to f32 first, then negate.

🐛 Proposed fix
-        descent: f32::from(-face.descender()) * scale,
+        descent: -f32::from(face.descender()) * scale,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
descent: f32::from(-face.descender()) * scale,
descent: -f32::from(face.descender()) * scale,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/textlayout/src/resolve.rs` at line 186, Update the descent calculation
to convert face.descender() to f32 before applying negation, avoiding i16::MIN
overflow while preserving the existing scaling behavior.

Comment on lines +55 to +81
const FORBIDDEN: &[&str] = &[
// Ambient font discovery — the environment is a manifest of bytes.
"fontdb",
"core_text",
"CoreText",
"dwrite",
"fontconfig",
// Render contracts and backends stay out of the resolver.
"rframe",
"websem",
"n0_model",
"csscascade",
"stylo",
"skia",
// Ambient inputs: resolution is a pure function of declared inputs.
"std::fs",
"std::net",
"std::env",
"std::time",
"std::io",
"SystemTime",
"Instant",
// Brace and glob imports could smuggle any of the above past a
// path-shaped needle; single-path imports only.
"use std::{",
"use std::*",
];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject compile-time ambient inputs.

include_bytes!, include_str!, env!, and option_env! bypass this denylist. These macros can embed undeclared files or environment values while this test still passes.

Proposed fix
         "Instant",
+        "include_bytes!",
+        "include_str!",
+        "env!",
+        "option_env!",

As per coding guidelines, never allow an unsupported construct to produce a silent wrong pixel.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const FORBIDDEN: &[&str] = &[
// Ambient font discovery — the environment is a manifest of bytes.
"fontdb",
"core_text",
"CoreText",
"dwrite",
"fontconfig",
// Render contracts and backends stay out of the resolver.
"rframe",
"websem",
"n0_model",
"csscascade",
"stylo",
"skia",
// Ambient inputs: resolution is a pure function of declared inputs.
"std::fs",
"std::net",
"std::env",
"std::time",
"std::io",
"SystemTime",
"Instant",
// Brace and glob imports could smuggle any of the above past a
// path-shaped needle; single-path imports only.
"use std::{",
"use std::*",
];
const FORBIDDEN: &[&str] = &[
// Ambient font discovery — the environment is a manifest of bytes.
"fontdb",
"core_text",
"CoreText",
"dwrite",
"fontconfig",
// Render contracts and backends stay out of the resolver.
"rframe",
"websem",
"n0_model",
"csscascade",
"stylo",
"skia",
// Ambient inputs: resolution is a pure function of declared inputs.
"std::fs",
"std::net",
"std::env",
"std::time",
"std::io",
"SystemTime",
"Instant",
"include_bytes!",
"include_str!",
"env!",
"option_env!",
// Brace and glob imports could smuggle any of the above past a
// path-shaped needle; single-path imports only.
"use std::{",
"use std::*",
];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/textlayout/tests/architecture.rs` around lines 55 - 81, Extend the
FORBIDDEN denylist in the architecture test to reject the compile-time
ambient-input macros include_bytes!, include_str!, env!, and option_env!. Keep
the existing path-based checks and add entries that catch these macro usages so
undeclared files or environment values cannot bypass the resolver restrictions.

Source: Coding guidelines

@softmarshmallow
softmarshmallow merged commit 6ce9851 into main Aug 4, 2026
15 checks passed
@softmarshmallow
softmarshmallow deleted the text-1-textlayout branch August 4, 2026 12:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant