From d0694bfb74018f9dfc90368bb27fbba1f20df19f Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Tue, 28 Apr 2026 12:53:26 +0200 Subject: [PATCH 1/3] Add PDictionary.OpenFile and TryOpenFile methods Add convenience methods for loading plist files as PDictionary: - OpenFile: returns a PDictionary or throws (never returns null) - TryOpenFile: returns bool with PDictionary as out parameter Both have overloads accepting an 'out bool isBinary' parameter. Add comprehensive tests covering valid dictionaries, non-dictionary plists, missing files, and invalid content. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Xamarin.MacDev/PListObject.cs | 53 ++++++++++++- tests/PListObjectTests.cs | 140 ++++++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+), 1 deletion(-) diff --git a/Xamarin.MacDev/PListObject.cs b/Xamarin.MacDev/PListObject.cs index f59baad..34bceaa 100644 --- a/Xamarin.MacDev/PListObject.cs +++ b/Xamarin.MacDev/PListObject.cs @@ -769,6 +769,57 @@ static int IndexOf (byte [] haystack, int startIndex, byte [] needle) return (PDictionary?) PObject.FromFile (fileName, out isBinary); } + /// + /// Opens a plist file and returns a PDictionary. + /// Throws if the file does not exist, cannot be parsed, or does not contain a dictionary. + /// + public static PDictionary OpenFile (string fileName) + { + return OpenFile (fileName, out _); + } + + /// + /// Opens a plist file and returns a PDictionary. + /// Throws if the file does not exist, cannot be parsed, or does not contain a dictionary. + /// + public static PDictionary OpenFile (string fileName, out bool isBinary) + { + var result = PObject.FromFile (fileName, out isBinary); + if (result is PDictionary dict) + return dict; + throw new FormatException ($"The plist file '{fileName}' does not contain a dictionary (got {result?.GetType ().Name ?? "null"})."); + } + + /// + /// Tries to open a plist file as a PDictionary. + /// Returns false if the file does not exist, cannot be parsed, or does not contain a dictionary. + /// + public static bool TryOpenFile (string fileName, [NotNullWhen (true)] out PDictionary? result) + { + return TryOpenFile (fileName, out result, out _); + } + + /// + /// Tries to open a plist file as a PDictionary. + /// Returns false if the file does not exist, cannot be parsed, or does not contain a dictionary. + /// + public static bool TryOpenFile (string fileName, [NotNullWhen (true)] out PDictionary? result, out bool isBinary) + { + result = null; + isBinary = false; + + try { + var obj = PObject.FromFile (fileName, out isBinary); + if (obj is PDictionary dict) { + result = dict; + return true; + } + return false; + } catch { + return false; + } + } + public static PDictionary? FromBinaryXml (string fileName) { return FromBinaryXml (File.ReadAllBytes (fileName)); @@ -1919,7 +1970,7 @@ protected override void ReadObjectHead () try { CurrentType = (PlistType) Enum.Parse (typeof (PlistType), reader!.LocalName); } catch (Exception ex) { - throw new ArgumentException (string.Format ("Failed to parse PList data type: {0}", reader?.LocalName), ex); + throw new FormatException (string.Format ("Failed to parse PList data type: {0}", reader?.LocalName), ex); } } diff --git a/tests/PListObjectTests.cs b/tests/PListObjectTests.cs index 8b81195..bde3046 100644 --- a/tests/PListObjectTests.cs +++ b/tests/PListObjectTests.cs @@ -142,6 +142,146 @@ public void TestStrings () Assert.That (plist.TryGetStringValue ("✅", out var emojiValue), Is.True); Assert.That (emojiValue, Is.EqualTo ("❌")); } + + string CreateTempPlistFile (string content) + { + var path = Path.Combine (Path.GetTempPath (), Path.GetRandomFileName () + ".plist"); + File.WriteAllText (path, content); + return path; + } + + static readonly string SamplePlistXml = @" + + + + Name + Test + +"; + + static readonly string ArrayPlistXml = @" + + + + A + +"; + + [Test] + public void OpenFile_ValidDictionary () + { + var path = CreateTempPlistFile (SamplePlistXml); + try { + var dict = PDictionary.OpenFile (path); + Assert.That (dict, Is.Not.Null); + Assert.That (dict.TryGetStringValue ("Name", out var name), Is.True); + Assert.That (name, Is.EqualTo ("Test")); + } finally { + File.Delete (path); + } + } + + [Test] + public void OpenFile_ValidDictionaryWithIsBinary () + { + var path = CreateTempPlistFile (SamplePlistXml); + try { + var dict = PDictionary.OpenFile (path, out bool isBinary); + Assert.That (isBinary, Is.False); + Assert.That (dict, Is.Not.Null); + } finally { + File.Delete (path); + } + } + + [Test] + public void OpenFile_FileNotFound () + { + var path = Path.Combine (Path.GetTempPath (), "nonexistent-" + Path.GetRandomFileName () + ".plist"); + Assert.Throws (() => PDictionary.OpenFile (path)); + } + + [Test] + public void OpenFile_NotADictionary () + { + var path = CreateTempPlistFile (ArrayPlistXml); + try { + var ex = Assert.Throws (() => PDictionary.OpenFile (path)); + Assert.That (ex.Message, Does.Contain ("does not contain a dictionary")); + } finally { + File.Delete (path); + } + } + + [Test] + public void OpenFile_InvalidContent () + { + var path = CreateTempPlistFile ("this is not valid plist content"); + try { + Assert.Throws (() => PDictionary.OpenFile (path)); + } finally { + File.Delete (path); + } + } + + [Test] + public void TryOpenFile_ValidDictionary () + { + var path = CreateTempPlistFile (SamplePlistXml); + try { + Assert.That (PDictionary.TryOpenFile (path, out var dict), Is.True); + Assert.That (dict, Is.Not.Null); + Assert.That (dict.TryGetStringValue ("Name", out var name), Is.True); + Assert.That (name, Is.EqualTo ("Test")); + } finally { + File.Delete (path); + } + } + + [Test] + public void TryOpenFile_ValidDictionaryWithIsBinary () + { + var path = CreateTempPlistFile (SamplePlistXml); + try { + Assert.That (PDictionary.TryOpenFile (path, out var dict, out bool isBinary), Is.True); + Assert.That (isBinary, Is.False); + Assert.That (dict, Is.Not.Null); + } finally { + File.Delete (path); + } + } + + [Test] + public void TryOpenFile_FileNotFound () + { + var path = Path.Combine (Path.GetTempPath (), "nonexistent-" + Path.GetRandomFileName () + ".plist"); + Assert.That (PDictionary.TryOpenFile (path, out var dict), Is.False); + Assert.That (dict, Is.Null); + } + + [Test] + public void TryOpenFile_NotADictionary () + { + var path = CreateTempPlistFile (ArrayPlistXml); + try { + Assert.That (PDictionary.TryOpenFile (path, out var dict), Is.False); + Assert.That (dict, Is.Null); + } finally { + File.Delete (path); + } + } + + [Test] + public void TryOpenFile_InvalidContent () + { + var path = CreateTempPlistFile ("this is not valid plist content"); + try { + Assert.That (PDictionary.TryOpenFile (path, out var dict), Is.False); + Assert.That (dict, Is.Null); + } finally { + File.Delete (path); + } + } } } From a5edf5b357a76531e9f6b6dd896994b268c33ec5 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Tue, 28 Apr 2026 16:50:30 +0200 Subject: [PATCH 2/3] Add tests showing PObject.FromFile returns non-PDictionary types PObject.FromFile can return PArray, PString, PNumber, PReal, PBoolean, PDate, or PData depending on the root element of the plist file. Add tests for each case to demonstrate this behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/PListObjectTests.cs | 129 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/tests/PListObjectTests.cs b/tests/PListObjectTests.cs index bde3046..0b56647 100644 --- a/tests/PListObjectTests.cs +++ b/tests/PListObjectTests.cs @@ -165,6 +165,42 @@ string CreateTempPlistFile (string content) A +"; + + static readonly string StringPlistXml = @" + + +hello world +"; + + static readonly string IntegerPlistXml = @" + + +42 +"; + + static readonly string RealPlistXml = @" + + +3.14 +"; + + static readonly string BooleanPlistXml = @" + + + +"; + + static readonly string DatePlistXml = @" + + +2024-01-15T10:30:00Z +"; + + static readonly string DataPlistXml = @" + + +AQID "; [Test] @@ -282,6 +318,99 @@ public void TryOpenFile_InvalidContent () File.Delete (path); } } + + [Test] + public void FromFile_ReturnsArray () + { + var path = CreateTempPlistFile (ArrayPlistXml); + try { + var obj = PObject.FromFile (path, out bool isBinary); + Assert.That (obj, Is.InstanceOf ()); + var array = (PArray) obj!; + Assert.That (array.Count, Is.EqualTo (1)); + Assert.That (((PString) array [0]).Value, Is.EqualTo ("A")); + } finally { + File.Delete (path); + } + } + + [Test] + public void FromFile_ReturnsString () + { + var path = CreateTempPlistFile (StringPlistXml); + try { + var obj = PObject.FromFile (path, out bool isBinary); + Assert.That (obj, Is.InstanceOf ()); + Assert.That (((PString) obj!).Value, Is.EqualTo ("hello world")); + } finally { + File.Delete (path); + } + } + + [Test] + public void FromFile_ReturnsInteger () + { + var path = CreateTempPlistFile (IntegerPlistXml); + try { + var obj = PObject.FromFile (path, out bool isBinary); + Assert.That (obj, Is.InstanceOf ()); + Assert.That (((PNumber) obj!).Value, Is.EqualTo (42)); + } finally { + File.Delete (path); + } + } + + [Test] + public void FromFile_ReturnsReal () + { + var path = CreateTempPlistFile (RealPlistXml); + try { + var obj = PObject.FromFile (path, out bool isBinary); + Assert.That (obj, Is.InstanceOf ()); + Assert.That (((PReal) obj!).Value, Is.EqualTo (3.14)); + } finally { + File.Delete (path); + } + } + + [Test] + public void FromFile_ReturnsBoolean () + { + var path = CreateTempPlistFile (BooleanPlistXml); + try { + var obj = PObject.FromFile (path, out bool isBinary); + Assert.That (obj, Is.InstanceOf ()); + Assert.That (((PBoolean) obj!).Value, Is.True); + } finally { + File.Delete (path); + } + } + + [Test] + public void FromFile_ReturnsDate () + { + var path = CreateTempPlistFile (DatePlistXml); + try { + var obj = PObject.FromFile (path, out bool isBinary); + Assert.That (obj, Is.InstanceOf ()); + Assert.That (((PDate) obj!).Value, Is.EqualTo (new DateTime (2024, 1, 15, 10, 30, 0, DateTimeKind.Utc))); + } finally { + File.Delete (path); + } + } + + [Test] + public void FromFile_ReturnsData () + { + var path = CreateTempPlistFile (DataPlistXml); + try { + var obj = PObject.FromFile (path, out bool isBinary); + Assert.That (obj, Is.InstanceOf ()); + Assert.That (((PData) obj!).Value, Is.EqualTo (new byte [] { 1, 2, 3 })); + } finally { + File.Delete (path); + } + } } } From 3a063f8d6fd184e548ac8a1446acd1865eb7c68b Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Tue, 28 Apr 2026 16:56:18 +0200 Subject: [PATCH 3/3] Add tests for [Try]OpenFile with non-dictionary root plist types Verify that OpenFile throws FormatException (with the actual type name in the message) and TryOpenFile returns false for all non-dict root plist types: array, string, integer, real, boolean, date, and data. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/PListObjectTests.cs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/PListObjectTests.cs b/tests/PListObjectTests.cs index 0b56647..92421ba 100644 --- a/tests/PListObjectTests.cs +++ b/tests/PListObjectTests.cs @@ -319,6 +319,41 @@ public void TryOpenFile_InvalidContent () } } + static readonly object [] NonDictPlistCases = new object [] { + new object [] { "array", ArrayPlistXml, "PArray" }, + new object [] { "string", StringPlistXml, "PString" }, + new object [] { "integer", IntegerPlistXml, "PNumber" }, + new object [] { "real", RealPlistXml, "PReal" }, + new object [] { "boolean", BooleanPlistXml, "PBoolean" }, + new object [] { "date", DatePlistXml, "PDate" }, + new object [] { "data", DataPlistXml, "PData" }, + }; + + [TestCaseSource (nameof (NonDictPlistCases))] + public void OpenFile_NonDictRoot_Throws (string label, string xml, string expectedTypeName) + { + var path = CreateTempPlistFile (xml); + try { + var ex = Assert.Throws (() => PDictionary.OpenFile (path)); + Assert.That (ex.Message, Does.Contain ("does not contain a dictionary")); + Assert.That (ex.Message, Does.Contain (expectedTypeName)); + } finally { + File.Delete (path); + } + } + + [TestCaseSource (nameof (NonDictPlistCases))] + public void TryOpenFile_NonDictRoot_ReturnsFalse (string label, string xml, string expectedTypeName) + { + var path = CreateTempPlistFile (xml); + try { + Assert.That (PDictionary.TryOpenFile (path, out var dict), Is.False); + Assert.That (dict, Is.Null); + } finally { + File.Delete (path); + } + } + [Test] public void FromFile_ReturnsArray () {