csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
257 lines
12 KiB
257 lines
12 KiB
using Avalonia.UnitTests;
|
|
using Xunit;
|
|
|
|
namespace Avalonia.Base.UnitTests.Media.Fonts
|
|
{
|
|
/// <summary>
|
|
/// Characterization tests for the cmap / name / post robustness of the GlyphTypeface parsers,
|
|
/// using the <see cref="SyntheticFont"/> / <see cref="BigEndianBuffer"/> harness. A present-but-
|
|
/// malformed cosmetic table must degrade to the same fallback as an absent one rather than denying
|
|
/// the whole font, and cmap parsing must not overflow or mis-select a subtable.
|
|
/// </summary>
|
|
public class MalformedCmapNamePostTests
|
|
{
|
|
// ── present-but-malformed cosmetic tables must not deny the font ──
|
|
//
|
|
// A *missing* 'post' / 'name' table is handled gracefully (see the two "Missing_*" tests
|
|
// below), and so is a *present-but-malformed* (truncated) one: the loader isolates the
|
|
// over-reading parse and degrades to the same fallback as an absent table.
|
|
|
|
[Fact]
|
|
public void Truncated_Post_Table_Does_Not_Deny_The_Font()
|
|
{
|
|
// Keep only the 4-byte version field; reading the rest of the header over-runs.
|
|
var font = SyntheticFont.FromAsset(SyntheticFont.Assets.InterRegular).Truncate("post", 4);
|
|
|
|
var typeface = font.TryCreateGlyphTypeface();
|
|
|
|
// A malformed 'post' (underline / italic-angle / fixed-pitch hints only) degrades to
|
|
// defaults; the rest of the font — including the intact 'name' — still loads.
|
|
Assert.NotNull(typeface);
|
|
Assert.Equal("Inter", typeface!.FamilyName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Truncated_Name_Table_Falls_Back_To_Unknown_Family()
|
|
{
|
|
// Keep only format + count; reading stringOffset and the record array over-runs.
|
|
var font = SyntheticFont.FromAsset(SyntheticFont.Assets.InterRegular).Truncate("name", 4);
|
|
|
|
var typeface = font.TryCreateGlyphTypeface();
|
|
|
|
// A malformed 'name' degrades to no name table, so the family name falls back to "unknown"
|
|
// instead of denying a renderable font.
|
|
Assert.NotNull(typeface);
|
|
Assert.Equal("unknown", typeface!.FamilyName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Missing_Post_Table_Still_Loads_The_Font()
|
|
{
|
|
// Documents the already-correct path: an *absent* cosmetic table is tolerated.
|
|
var font = SyntheticFont.FromAsset(SyntheticFont.Assets.InterRegular).Remove("post");
|
|
|
|
var typeface = font.TryCreateGlyphTypeface();
|
|
|
|
Assert.NotNull(typeface);
|
|
Assert.Equal("Inter", typeface!.FamilyName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Missing_Name_Table_Falls_Back_To_Unknown_Family()
|
|
{
|
|
// Documents the already-correct path: an *absent* name table yields a usable typeface
|
|
// with a fallback family name.
|
|
var font = SyntheticFont.FromAsset(SyntheticFont.Assets.InterRegular).Remove("name");
|
|
|
|
var typeface = font.TryCreateGlyphTypeface();
|
|
|
|
Assert.NotNull(typeface);
|
|
Assert.Equal("unknown", typeface!.FamilyName);
|
|
}
|
|
|
|
// ── cmap format 12 nGroups*12 overflow denies the whole font ──
|
|
|
|
[Fact]
|
|
public void Cmap_Format12_NGroups_Overflow_Does_Not_Deny_The_Font()
|
|
{
|
|
// numGroups = 0x20000000 would make `numGroups * 12` overflow int32 to a negative slice
|
|
// length and throw out of the (throwing) CmapTable.Load.
|
|
var font = SyntheticFont.FromAsset(SyntheticFont.Assets.InterRegular)
|
|
.Replace("cmap", BuildCmapWithOverflowingFormat12(numGroups: 0x20000000));
|
|
|
|
var typeface = font.TryCreateGlyphTypeface();
|
|
|
|
// The group count is now computed in long and clamped to what the table holds, so the bad
|
|
// subtable yields empty coverage rather than throwing — the font still loads.
|
|
Assert.NotNull(typeface);
|
|
}
|
|
|
|
// ── cmap format 12 hostile group *contents* must not hang range enumeration ──
|
|
|
|
[Fact]
|
|
public void Cmap_Format12_Huge_Group_End_Is_Clamped_To_The_Unicode_Range()
|
|
{
|
|
// endCharCode = 0x7FFFFFFF used to reach the per-codepoint dictionary loops unclamped:
|
|
// `cp <= end` with end == int.MaxValue is a tautology, so Count / Keys / the enumerator
|
|
// never terminated. The group is now clamped to the Unicode range at the choke point.
|
|
var font = SyntheticFont.FromAsset(SyntheticFont.Assets.InterRegular)
|
|
.Replace("cmap", BuildFormat12Cmap(
|
|
(Start: 0x41u, End: 0x7FFFFFFFu, StartGlyph: 1u)));
|
|
|
|
var typeface = font.TryCreateGlyphTypeface();
|
|
Assert.NotNull(typeface);
|
|
|
|
var dictionary = typeface!.CharacterToGlyphMap.AsReadOnlyDictionary();
|
|
|
|
// Terminates (clamped to ≤ 0x10FFFF) and still exposes the in-range part of the group.
|
|
Assert.True(dictionary.Count > 0);
|
|
Assert.True(dictionary.ContainsKey(0x41));
|
|
}
|
|
|
|
[Fact]
|
|
public void Cmap_Format12_Inverted_Group_Is_Skipped_Not_Terminal()
|
|
{
|
|
// The second group is inverted (start > end). It must enumerate as an empty range —
|
|
// not throw, not end enumeration early, and not hide the first (valid) group.
|
|
var font = SyntheticFont.FromAsset(SyntheticFont.Assets.InterRegular)
|
|
.Replace("cmap", BuildFormat12Cmap(
|
|
(Start: 0x41u, End: 0x41u, StartGlyph: 5u),
|
|
(Start: 0x1000u, End: 0x100u, StartGlyph: 6u)));
|
|
|
|
var typeface = font.TryCreateGlyphTypeface();
|
|
Assert.NotNull(typeface);
|
|
|
|
var dictionary = typeface!.CharacterToGlyphMap.AsReadOnlyDictionary();
|
|
|
|
Assert.Equal(1, dictionary.Count);
|
|
Assert.True(dictionary.ContainsKey(0x41));
|
|
}
|
|
|
|
// ── cmap Format-4 subtable selection prefers Unicode over Symbol ──
|
|
|
|
[Theory]
|
|
[InlineData(true)] // Symbol subtable listed first
|
|
[InlineData(false)] // Unicode subtable listed first
|
|
public void Format4_Subtable_Selection_Prefers_Unicode_Over_Symbol(bool symbolFirst)
|
|
{
|
|
// Two Windows-platform Format-4 subtables: a Symbol (encoding 0) one that maps only the
|
|
// PUA codepoint 0xF041, and a Unicode-BMP (encoding 1) one that maps 'A'. The selection
|
|
// now scores the Symbol encoding worse than Unicode, so the Unicode subtable wins
|
|
// regardless of directory order.
|
|
var font = SyntheticFont.FromAsset(SyntheticFont.Assets.InterRegular)
|
|
.Replace("cmap", BuildDualFormat4Cmap(symbolFirst));
|
|
|
|
var typeface = font.TryCreateGlyphTypeface();
|
|
Assert.NotNull(typeface);
|
|
|
|
// ASCII 'A' now resolves in BOTH orderings — encoding, not directory order, decides.
|
|
Assert.True(typeface!.CharacterToGlyphMap.ContainsGlyph('A'));
|
|
}
|
|
|
|
private static byte[] BuildCmapWithOverflowingFormat12(uint numGroups)
|
|
{
|
|
// Format-12 subtable: format(2) reserved(2) length(4) language(4) numGroups(4) groups[…].
|
|
// length is honest about the 16-byte buffer, so the length-slice succeeds and the
|
|
// overflow surfaces at the group-array slice (the exact path under test).
|
|
var subtable = new BigEndianBuffer()
|
|
.UInt16(12) // format
|
|
.UInt16(0) // reserved
|
|
.UInt32(16) // length (header only)
|
|
.UInt32(0) // language
|
|
.UInt32(numGroups) // numGroups
|
|
.ToArray();
|
|
|
|
// cmap header: version(2) numTables(2), then one EncodingRecord: platform(2) encoding(2) offset(4).
|
|
var cmap = new BigEndianBuffer();
|
|
cmap.UInt16(0); // version
|
|
cmap.UInt16(1); // numTables
|
|
cmap.UInt16(3); // platformID = Windows
|
|
cmap.UInt16(10); // encodingID = UCS-4 (any value works; format 12 is selected regardless)
|
|
var offsetPos = cmap.ReserveOffset32();
|
|
cmap.PatchUInt32(offsetPos, (uint)cmap.Position);
|
|
cmap.Bytes(subtable);
|
|
|
|
return cmap.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds a cmap with a single format-12 subtable carrying the given sequential map groups
|
|
/// (which must be ordered by start code for the lookup binary search).
|
|
/// </summary>
|
|
private static byte[] BuildFormat12Cmap(params (uint Start, uint End, uint StartGlyph)[] groups)
|
|
{
|
|
var subtable = new BigEndianBuffer()
|
|
.UInt16(12) // format
|
|
.UInt16(0) // reserved
|
|
.UInt32((uint)(16 + groups.Length * 12)) // length
|
|
.UInt32(0) // language
|
|
.UInt32((uint)groups.Length); // numGroups
|
|
|
|
foreach (var (start, end, startGlyph) in groups)
|
|
{
|
|
subtable.UInt32(start).UInt32(end).UInt32(startGlyph);
|
|
}
|
|
|
|
var cmap = new BigEndianBuffer();
|
|
cmap.UInt16(0); // version
|
|
cmap.UInt16(1); // numTables
|
|
cmap.UInt16(3); // platformID = Windows
|
|
cmap.UInt16(10); // encodingID = UCS-4
|
|
cmap.UInt32(12); // offset: header(4) + one EncodingRecord(8)
|
|
cmap.Bytes(subtable.ToArray());
|
|
|
|
return cmap.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds a cmap with two Windows-platform Format-4 subtables — a Symbol (encoding 0) one
|
|
/// mapping the PUA codepoint 0xF041 and a Unicode-BMP (encoding 1) one mapping 'A' — ordered
|
|
/// per <paramref name="symbolFirst"/>.
|
|
/// </summary>
|
|
private static byte[] BuildDualFormat4Cmap(bool symbolFirst)
|
|
{
|
|
var symbol = BuildSingleCharFormat4(charCode: 0xF041, glyph: 7);
|
|
var unicode = BuildSingleCharFormat4(charCode: 'A', glyph: 5);
|
|
|
|
var firstSub = symbolFirst ? symbol : unicode;
|
|
var firstEncoding = symbolFirst ? 0 : 1; // Symbol = 0, UnicodeBMP = 1
|
|
var secondSub = symbolFirst ? unicode : symbol;
|
|
var secondEncoding = symbolFirst ? 1 : 0;
|
|
|
|
var cmap = new BigEndianBuffer();
|
|
cmap.UInt16(0); // version
|
|
cmap.UInt16(2); // numTables
|
|
|
|
// Two 8-byte EncodingRecords follow the 4-byte header, so the subtables start at offset 20.
|
|
const int subtablesStart = 4 + 2 * 8;
|
|
cmap.UInt16(3); cmap.UInt16(firstEncoding); cmap.UInt32(subtablesStart);
|
|
cmap.UInt16(3); cmap.UInt16(secondEncoding); cmap.UInt32((uint)(subtablesStart + firstSub.Length));
|
|
cmap.Bytes(firstSub);
|
|
cmap.Bytes(secondSub);
|
|
|
|
return cmap.ToArray();
|
|
}
|
|
|
|
/// <summary>Builds a minimal Format-4 cmap subtable mapping a single <paramref name="charCode"/> to <paramref name="glyph"/>.</summary>
|
|
private static byte[] BuildSingleCharFormat4(int charCode, int glyph)
|
|
{
|
|
// Two segments: [charCode, charCode] and the mandatory terminal [0xFFFF, 0xFFFF].
|
|
// No glyphIdArray — the glyph comes from idDelta (idRangeOffset = 0). Total length 32.
|
|
return new BigEndianBuffer()
|
|
.UInt16(4) // format
|
|
.UInt16(32) // length
|
|
.UInt16(0) // language
|
|
.UInt16(4) // segCountX2 (segCount = 2)
|
|
.UInt16(4) // searchRange
|
|
.UInt16(1) // entrySelector
|
|
.UInt16(0) // rangeShift
|
|
.UInt16(charCode).UInt16(0xFFFF) // endCode[2]
|
|
.UInt16(0) // reservedPad
|
|
.UInt16(charCode).UInt16(0xFFFF) // startCode[2]
|
|
.UInt16((glyph - charCode) & 0xFFFF).UInt16(1) // idDelta[2]
|
|
.UInt16(0).UInt16(0) // idRangeOffset[2]
|
|
.ToArray();
|
|
}
|
|
}
|
|
}
|
|
|