Repository: AvaloniaUI/Avalonia (C#)

Issue:
ControlCatalog.Desktop crashes when switching to TextBox/TextBlock Tab
### Describe the bug

I used the master branch to compile and run Control Catalog.Desktop with Visual Studio, and the crash described in the title occurred. This happens regardless of debug or release mode. There is no such issue when compiling and running with 11.3.4 branch.

**Debug mode:** Crash in \Avalonia\src\Avalonia.Base\Media\TextFormatting\FormattingObjectPool.cs

<img width="945" height="377" alt="Image" src="https://github.com/user-attachments/assets/83daa9bb-7b6c-4eab-8fd7-6ec290cd21bc" />

**Release mode:** Crash in \Avalonia\src\Avalonia.Base\Media\Typeface.cs

<img width="1355" height="365" alt="Image" src="https://github.com/user-attachments/assets/385b52c5-3843-4a77-a834-28fa085be5b2" />


### To Reproduce

1. Compile and run ControlCatalog.Desktop
2. Click TextBox or TextBlock Tab

### Expected behavior

_No response_

### Avalonia version

master

### OS

Windows

### Additional context

Windows 11 10.0.26100.4946


Discussion:
This does not validate the baseUri: https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Base/Media/FontFamily.cs#L58
Caused by: #19494



Patch:
diff --git a/src/Avalonia.Base/Media/FontFamily.cs b/src/Avalonia.Base/Media/FontFamily.cs
index 722f806cac0..aa2ce05bd45 100644
--- a/src/Avalonia.Base/Media/FontFamily.cs
+++ b/src/Avalonia.Base/Media/FontFamily.cs
@@ -36,6 +36,11 @@ public FontFamily(Uri? baseUri, string name)
                 throw new ArgumentNullException(nameof(name));
             }
 
+            if (baseUri != null && !baseUri.IsAbsoluteUri)
+            {
+                throw new ArgumentException("Base uri must be an absolute uri.", nameof(baseUri));
+            }
+
             var fontSources = GetFontSourceIdentifier(name);
 
             FamilyNames = new FamilyNameCollection(fontSources);
@@ -46,20 +51,15 @@ public FontFamily(Uri? baseUri, string name)
 
                 if (singleSource.Source is Uri source)
                 {
-                    if (baseUri != null && !baseUri.IsAbsoluteUri)
+                    if (source.IsAbsoluteUri)
                     {
-                        throw new ArgumentException("Base uri must be an absolute uri.", nameof(baseUri));
+                        Key = new FontFamilyKey(source);
                     }
-
-                    Key = new FontFamilyKey(source, baseUri);
-                }
-                else
-                {
-                    if(baseUri != null && baseUri.IsAbsoluteUri)
+                    else
                     {
-                        Key = new FontFamilyKey(baseUri);
+                        Key = new FontFamilyKey(source, baseUri);
                     }
-                }
+                }               
             }
             else
             {
@@ -138,7 +138,7 @@ private static FrugalStructList<FontSourceIdentifier> GetFontSourceIdentifier(st
                 var segment = segments[i];
                 var innerSegments = segment.Split('#');
 
-                FontSourceIdentifier identifier;
+                FontSourceIdentifier identifier = new FontSourceIdentifier(name, null);
 
                 switch (innerSegments.Length)
                 {
@@ -159,19 +159,19 @@ private static FrugalStructList<FontSourceIdentifier> GetFontSourceIdentifier(st
                             }
                             else
                             {
-                                var source = path.StartsWith("/", StringComparison.Ordinal)
-                                   ? new Uri(path, UriKind.Relative)
-                                   : new Uri(path, UriKind.RelativeOrAbsolute);
-
-                                identifier = new FontSourceIdentifier(innerName, source);
-                            }                              
-
-                            break;
-                        }
+                                if (path.Contains('/') && Uri.TryCreate(path, UriKind.Relative, out var source))
+                                {
+                                    identifier = new FontSourceIdentifier(innerName, source);
+                                }
+                                else
+                                {
+                                    if (Uri.TryCreate(path, UriKind.Absolute, out source))
+                                    {
+                                        identifier = new FontSourceIdentifier(innerName, source);
+                                    }
+                                }
+                            }
 
-                    default:
-                        {
-                            identifier = new FontSourceIdentifier(name, null);
                             break;
                         }
                 }
diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs
index 7310ab19a11..dfce362297b 100644
--- a/src/Avalonia.Base/Media/FontManager.cs
+++ b/src/Avalonia.Base/Media/FontManager.cs
@@ -273,8 +273,6 @@ public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fon
             //Try to match against fallbacks first
             if (fontFamily?.Key != null)
             {
-                var fontUri = fontFamily.Key.Source.EnsureAbsolute(fontFamily.Key.BaseUri);
-
                 if (fontFamily.Key is CompositeFontFamilyKey compositeKey)
                 {
                     for (int i = 0; i < compositeKey.Keys.Count; i++)
@@ -296,6 +294,8 @@ public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fon
                     }
                 }
 
+                var fontUri = fontFamily.Key.Source.EnsureAbsolute(fontFamily.Key.BaseUri);
+
                 if (fontUri.IsFontCollection())
                 {
                     if (TryGetFontCollection(fontUri, out var fontCollection) &&
diff --git a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs
index 038332c2da8..222a514ed13 100644
--- a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs
+++ b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs
@@ -35,7 +35,7 @@ public virtual bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight
                 {
                     if (glyphTypeface.TryGetGlyph((uint)codepoint, out _))
                     {
-                        match = new Typeface(new FontFamily(Key, "#" + glyphTypeface.FamilyName), style, weight, stretch);
+                        match = new Typeface(new FontFamily(null, Key.AbsoluteUri + "#" + glyphTypeface.FamilyName), style, weight, stretch);
 
                         return true;
                     }
@@ -57,7 +57,7 @@ public virtual bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight
                 {
                     if (glyphTypeface.TryGetGlyph((uint)codepoint, out _))
                     {
-                        match = new Typeface(new FontFamily(Key, "#" + glyphTypeface.FamilyName) , style, weight, stretch);
+                        match = new Typeface(new FontFamily(null, Key.AbsoluteUri + "#" + glyphTypeface.FamilyName), style, weight, stretch);
 
                         return true;
                     }


Test patch:
diff --git a/tests/Avalonia.Base.UnitTests/Media/FontFamilyTests.cs b/tests/Avalonia.Base.UnitTests/Media/FontFamilyTests.cs
index 73c46a92959..7990fb14265 100644
--- a/tests/Avalonia.Base.UnitTests/Media/FontFamilyTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Media/FontFamilyTests.cs
@@ -121,12 +121,12 @@ public void Should_Create_FontFamily_From_Uri(string name)
         }
 
         [Theory]
-        [InlineData("resm:Avalonia.Visuals.UnitTests.Assets.Fonts", "#MyFont")]
-        [InlineData("avares://Avalonia.Visuals.UnitTests/Assets/Fonts", "#MyFont")]
+        [InlineData(null, "resm:Avalonia.Visuals.UnitTests.Assets.Fonts#MyFont")]
+        [InlineData("avares://Avalonia.Visuals.UnitTests/Assets/Fonts", "/#MyFont")]
         [InlineData("avares://Avalonia.Visuals.UnitTests", "/Assets/Fonts#MyFont")]
         public void Should_Create_FontFamily_From_Uri_With_Base_Uri(string @base, string name)
         {
-            var baseUri = new Uri(@base);
+            var baseUri = @base != null ? new Uri(@base) : null;
 
             var fontFamily = new FontFamily(baseUri, name);
 
@@ -134,5 +134,77 @@ public void Should_Create_FontFamily_From_Uri_With_Base_Uri(string @base, string
 
             Assert.NotNull(fontFamily.Key);
         }
+
+        [InlineData(null, "Arial", "Arial", null)]
+        [InlineData(null, "resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Manrope", "Manrope", "resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests")]
+        [InlineData(null, "avares://Avalonia.Fonts.Inter/Assets#Inter", "Inter", null)]
+        [InlineData("avares://Avalonia.Fonts.Inter", "/Assets#Inter", "Inter", "avares://Avalonia.Fonts.Inter/Assets")]
+        [InlineData("avares://ControlCatalog/MainWindow.xaml", "avares://Avalonia.Fonts.Inter/Assets#Inter", "Inter", "avares://Avalonia.Fonts.Inter/Assets")]
+        [Theory]
+        public void Should_Parse_FontFamily_With_BaseUri(string baseUri, string s, string expectedName, string expectedUri)
+        {
+            var b = baseUri is not null ? new Uri(baseUri) : null;
+
+            expectedUri = expectedUri is not null ? new Uri(expectedUri).AbsoluteUri : null;
+
+            var fontFamily = FontFamily.Parse(s, b);
+
+            Assert.Equal(expectedName, fontFamily.Name);
+
+            var key = fontFamily.Key;
+
+            if (expectedUri is not null)
+            {
+                Assert.NotNull(key);
+
+                if (key.BaseUri is not null)
+                {
+                    Assert.True(key.BaseUri.IsAbsoluteUri);
+                }
+
+                if (key.BaseUri is null)
+                {
+                    Assert.NotNull(key.Source);
+                    Assert.True(key.Source.IsAbsoluteUri);
+                }
+
+                var fontUri = key.BaseUri;
+
+                if (key.Source is Uri sourceUri)
+                {
+                    if (sourceUri.IsAbsoluteUri)
+                    {
+                        fontUri = sourceUri;
+                    }
+                    else
+                    {
+                        fontUri = new Uri(fontUri, sourceUri);
+                    }
+                }
+
+                Assert.Equal(expectedUri, fontUri.AbsoluteUri);
+            }
+        }
+
+        [InlineData("avares://MyAssembly/", "Some/Path/#FontName", "avares://MyAssembly/Some/Path/"), ]
+        [InlineData("avares://MyAssembly/", "./Some/Path/#FontName", "avares://MyAssembly/Some/Path/")]
+        [InlineData("avares://MyAssembly/sub/", "../Some/Path/#FontName", "avares://MyAssembly/Some/Path/")]
+        [Theory]
+        public void Should_Parse_Relative_Path(string baseUriString, string path, string expected)
+        {
+            var baseUri = new Uri(baseUriString, UriKind.Absolute);
+
+            var fontFamily = FontFamily.Parse(path, baseUri);
+
+            Assert.NotNull(fontFamily.Key);
+
+            Assert.NotNull(fontFamily.Key.BaseUri);
+
+            Assert.NotNull(fontFamily.Key.Source);
+
+            var actual = new Uri(fontFamily.Key.BaseUri, fontFamily.Key.Source);
+
+            Assert.Equal(expected, actual.AbsoluteUri);
+        }
     }
 }
diff --git a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs
index 8057b81ace4..da609478155 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs
@@ -409,5 +409,23 @@ public void Should_Use_FontCollection_MatchCharacter()
                 }
             }
         }
+
+        [InlineData("Arial")]
+        [InlineData("#Arial")]
+        [Win32Theory("Windows specific font")]
+        public void Should_Get_SystemFont_With_BaseUri(string name)
+        {
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
+            {
+                using (AvaloniaLocator.EnterScope())
+                {
+                    var fontFamily = new FontFamily(new Uri("avares://Avalonia.Skia.UnitTests/NotFound"), name);
+
+                    var glyphTypeface = new Typeface(fontFamily).GlyphTypeface;
+
+                    Assert.Equal("Arial", glyphTypeface.FamilyName);
+                }
+            }
+        }
     }
 }
diff --git a/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs
index 78895912dbc..24599c5a9b0 100644
--- a/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs
+++ b/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs
@@ -14,11 +14,11 @@ public class HarfBuzzFontManagerImpl : IFontManagerImpl
         private readonly string _defaultFamilyName;
 
         private static readonly Typeface _defaultTypeface =
-            new Typeface(new FontFamily(new Uri("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests", UriKind.Absolute), "Noto Mono"));
+            new Typeface(new FontFamily("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Mono"));
         private  static readonly Typeface _italicTypeface =
-            new Typeface(new FontFamily(new Uri("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests", UriKind.Absolute), "Noto Sans"));
+            new Typeface(new FontFamily("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Sans"));
         private  static readonly Typeface _emojiTypeface =
-            new Typeface(new FontFamily(new Uri("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests"), "Twitter Color Emoji"));
+            new Typeface(new FontFamily("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Twitter Color Emoji"));
 
         public HarfBuzzFontManagerImpl(string defaultFamilyName = "Noto Mono")
         {