From 214ee0caf3e5192bdf4cb151a98d5d5f53fc29fb Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Fri, 29 May 2026 16:10:25 +0100 Subject: [PATCH 1/2] Fix NullReferenceException and crash in ManifestExtensions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GetWKAppBundleIdentifier: handle null from GetNSExtensionAttributes. - SetWKAppBundleIdentifier: auto-create NSExtension/NSExtensionAttributes structure when setting a non-empty value (avoid silent no-op). - ParseDeviceFamilyFromNumber → TryParseDeviceFamilyFromNumber: unknown device family numbers are now logged and skipped (not coerced to IPhone), consistent with how the string parser already handles unknown values. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Xamarin.MacDev/ManifestExtensions.cs | 59 ++++++++++++---- tests/ManifestExtensionsTests.cs | 100 +++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 15 deletions(-) create mode 100644 tests/ManifestExtensionsTests.cs diff --git a/Xamarin.MacDev/ManifestExtensions.cs b/Xamarin.MacDev/ManifestExtensions.cs index c8eae8b..7fe63ec 100644 --- a/Xamarin.MacDev/ManifestExtensions.cs +++ b/Xamarin.MacDev/ManifestExtensions.cs @@ -218,21 +218,28 @@ public static IPhoneDeviceType GetUIDeviceFamily (this PDictionary dict) return GetUIDeviceFamily (dict, ManifestKeys.UIDeviceFamily); } - static AppleDeviceFamily ParseDeviceFamilyFromNumber (PNumber number) + static bool TryParseDeviceFamilyFromNumber (PNumber number, out AppleDeviceFamily family) { switch (number.Value) { case 1: - return AppleDeviceFamily.IPhone; + family = AppleDeviceFamily.IPhone; + return true; case 2: - return AppleDeviceFamily.IPad; + family = AppleDeviceFamily.IPad; + return true; case 3: - return AppleDeviceFamily.TV; + family = AppleDeviceFamily.TV; + return true; case 4: - return AppleDeviceFamily.Watch; + family = AppleDeviceFamily.Watch; + return true; case 6: - return AppleDeviceFamily.MacCatalystOptimizedForMac; + family = AppleDeviceFamily.MacCatalystOptimizedForMac; + return true; default: - throw new ArgumentOutOfRangeException (string.Format ("Unknown device family: {0}", number.Value)); + LoggingService.LogWarning ($"Skipped unknown device family: {number.Value}"); + family = default; + return false; } } @@ -278,11 +285,12 @@ public static IPhoneDeviceType GetUIDeviceFamily (this PDictionary dict, string val |= ParseDeviceTypeFromString (p); var number = element as PNumber; - if (number != null) - val |= ParseDeviceFamilyFromNumber (number).ToDeviceType (); + if (number != null && TryParseDeviceFamilyFromNumber (number, out var family)) + val |= family.ToDeviceType (); } } else if (value is PNumber) { - val |= ParseDeviceFamilyFromNumber ((PNumber) value).ToDeviceType (); + if (TryParseDeviceFamilyFromNumber ((PNumber) value, out var family)) + val |= family.ToDeviceType (); } else if (value is PString) { val |= ParseDeviceTypeFromString ((PString) value); } @@ -496,6 +504,23 @@ static PDictionary GetNSExtensionAttributes (this PDictionary dict) return extAtt; } + static PDictionary GetOrCreateNSExtensionAttributes (this PDictionary dict) + { + var ext = dict.Get ("NSExtension"); + if (ext == null) { + ext = new PDictionary (); + dict ["NSExtension"] = ext; + } + + var extAtt = ext.Get ("NSExtensionAttributes"); + if (extAtt == null) { + extAtt = new PDictionary (); + ext ["NSExtensionAttributes"] = extAtt; + } + + return extAtt; + } + #endregion #region Watch App Manifest Keys @@ -513,6 +538,8 @@ public static void SetWKWatchKitApp (this PDictionary dict, bool value) public static string GetWKAppBundleIdentifier (this PDictionary dict) { var extAtt = GetNSExtensionAttributes (dict); + if (extAtt == null) + return null; var str = extAtt.Get (ManifestKeys.WKAppBundleIdentifier); return str == null ? null : str.Value; @@ -520,12 +547,14 @@ public static string GetWKAppBundleIdentifier (this PDictionary dict) public static void SetWKAppBundleIdentifier (this PDictionary dict, string value) { - var extAtt = GetNSExtensionAttributes (dict); - - if (string.IsNullOrEmpty (value)) - extAtt.Remove (ManifestKeys.WKAppBundleIdentifier); - else + if (string.IsNullOrEmpty (value)) { + var extAtt = GetNSExtensionAttributes (dict); + if (extAtt != null) + extAtt.Remove (ManifestKeys.WKAppBundleIdentifier); + } else { + var extAtt = GetOrCreateNSExtensionAttributes (dict); extAtt [ManifestKeys.WKAppBundleIdentifier] = value; + } } public static string GetWKCompanionAppBundleIdentifier (this PDictionary dict) diff --git a/tests/ManifestExtensionsTests.cs b/tests/ManifestExtensionsTests.cs new file mode 100644 index 0000000..3e7c734 --- /dev/null +++ b/tests/ManifestExtensionsTests.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using NUnit.Framework; +using Xamarin.MacDev; + +namespace tests { + + [TestFixture] + public class ManifestExtensionsTests { + + [Test] + public void GetWKAppBundleIdentifier_ReturnsNull_WhenNSExtensionMissing () + { + var dict = new PDictionary (); + var result = dict.GetWKAppBundleIdentifier (); + Assert.That (result, Is.Null); + } + + [Test] + public void GetWKAppBundleIdentifier_ReturnsNull_WhenNSExtensionAttributesMissing () + { + var dict = new PDictionary (); + dict.Add ("NSExtension", new PDictionary ()); + var result = dict.GetWKAppBundleIdentifier (); + Assert.That (result, Is.Null); + } + + [Test] + public void GetWKAppBundleIdentifier_ReturnsValue_WhenPresent () + { + var dict = new PDictionary (); + var ext = new PDictionary (); + var extAttr = new PDictionary (); + extAttr.Add ("WKAppBundleIdentifier", new PString ("com.test.watchapp")); + ext.Add ("NSExtensionAttributes", extAttr); + dict.Add ("NSExtension", ext); + + var result = dict.GetWKAppBundleIdentifier (); + Assert.That (result, Is.EqualTo ("com.test.watchapp")); + } + + [Test] + public void SetWKAppBundleIdentifier_CreatesStructure_WhenNSExtensionMissing () + { + var dict = new PDictionary (); + dict.SetWKAppBundleIdentifier ("com.test.app"); + Assert.That (dict.GetWKAppBundleIdentifier (), Is.EqualTo ("com.test.app")); + } + + [Test] + public void SetWKAppBundleIdentifier_RemoveIsNoOp_WhenNSExtensionMissing () + { + var dict = new PDictionary (); + Assert.DoesNotThrow (() => dict.SetWKAppBundleIdentifier ("")); + } + + [Test] + public void GetUIDeviceFamily_SkipsUnknownDeviceFamilyNumber () + { + var dict = new PDictionary (); + var arr = new PArray (); + arr.Add (new PNumber (99)); + dict.Add ("UIDeviceFamily", arr); + + var result = dict.GetUIDeviceFamily ("UIDeviceFamily"); + Assert.That (result, Is.EqualTo (IPhoneDeviceType.NotSet)); + } + + [Test] + public void GetUIDeviceFamily_MixedKnownAndUnknown_SkipsUnknown () + { + var dict = new PDictionary (); + var arr = new PArray (); + arr.Add (new PNumber (2)); // IPad + arr.Add (new PNumber (99)); // unknown — should be skipped + dict.Add ("UIDeviceFamily", arr); + + var result = dict.GetUIDeviceFamily ("UIDeviceFamily"); + Assert.That (result.HasFlag (IPhoneDeviceType.IPad), Is.True); + Assert.That (result.HasFlag (IPhoneDeviceType.IPhone), Is.False); + } + + [Test] + public void GetUIDeviceFamily_ParsesKnownDeviceFamilies () + { + var dict = new PDictionary (); + var arr = new PArray (); + arr.Add (new PNumber (1)); + arr.Add (new PNumber (2)); + dict.Add ("UIDeviceFamily", arr); + + var result = dict.GetUIDeviceFamily ("UIDeviceFamily"); + Assert.That (result.HasFlag (IPhoneDeviceType.IPhone), Is.True); + Assert.That (result.HasFlag (IPhoneDeviceType.IPad), Is.True); + } + } +} From a27f25b35be4ea91bf6d3176ed0eae5eea2ae08b Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Fri, 29 May 2026 18:39:19 +0100 Subject: [PATCH 2/2] Fix misleading log message in TryParseDeviceFamilyFromNumber Changed 'Skipped unknown device family' to 'Ignoring unrecognized device family number' to make it clear the value is not mapped to any family and is simply discarded. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Xamarin.MacDev/ManifestExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Xamarin.MacDev/ManifestExtensions.cs b/Xamarin.MacDev/ManifestExtensions.cs index 7fe63ec..9948d01 100644 --- a/Xamarin.MacDev/ManifestExtensions.cs +++ b/Xamarin.MacDev/ManifestExtensions.cs @@ -237,7 +237,7 @@ static bool TryParseDeviceFamilyFromNumber (PNumber number, out AppleDeviceFamil family = AppleDeviceFamily.MacCatalystOptimizedForMac; return true; default: - LoggingService.LogWarning ($"Skipped unknown device family: {number.Value}"); + LoggingService.LogWarning ($"Ignoring unrecognized device family number: {number.Value}"); family = default; return false; }