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); + } + } } }