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..92421ba 100644
--- a/tests/PListObjectTests.cs
+++ b/tests/PListObjectTests.cs
@@ -142,6 +142,310 @@ 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
+
+";
+
+ 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]
+ 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);
+ }
+ }
+
+ 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 ()
+ {
+ 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);
+ }
+ }
}
}