// Copyright © 2017-2025 QL-Win Contributors
//
// This file is part of QuickLook program.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using System.Xml;
namespace QuickLook.Plugin.AppViewer.PackageParsers.Ipa;
///
/// https://github.com/animetrics/PlistCS
///
public static class Plist
{
private static readonly List offsetTable = [];
private static List objectTable = [];
private static int refCount;
private static int objRefSize;
private static int offsetByteSize;
private static long offsetTableOffset;
public static object ReadPlist(string path)
{
using var f = new FileStream(path, FileMode.Open, FileAccess.Read);
return ReadPlist(f, PlistType.Auto);
}
public static object ReadPlistSource(string source)
{
return ReadPlist(Encoding.UTF8.GetBytes(source));
}
public static object ReadPlist(byte[] data)
{
return ReadPlist(new MemoryStream(data), PlistType.Auto);
}
public static PlistType GetPlistType(Stream stream)
{
byte[] magicHeader = new byte[8];
stream.Read(magicHeader, 0, 8);
if (BitConverter.ToInt64(magicHeader, 0) == 3472403351741427810L)
{
return PlistType.Binary;
}
else
{
return PlistType.Xml;
}
}
public static object ReadPlist(Stream stream, PlistType type)
{
if (type == PlistType.Auto)
{
type = GetPlistType(stream);
stream.Seek(0, SeekOrigin.Begin);
}
if (type == PlistType.Binary)
{
using var reader = new BinaryReader(stream);
byte[] data = reader.ReadBytes((int)reader.BaseStream.Length);
return ReadBinary(data);
}
else
{
XmlDocument xml = new()
{
XmlResolver = null,
};
xml.Load(stream);
return ReadXml(xml);
}
}
public static void WriteXml(object value, string path)
{
using var writer = new StreamWriter(path);
writer.Write(WriteXml(value));
}
public static void WriteXml(object value, Stream stream)
{
using var writer = new StreamWriter(stream);
writer.Write(WriteXml(value));
}
public static string WriteXml(object value)
{
using var ms = new MemoryStream();
XmlWriterSettings xmlWriterSettings = new()
{
Encoding = new UTF8Encoding(false),
ConformanceLevel = ConformanceLevel.Document,
Indent = true,
};
using var xmlWriter = XmlWriter.Create(ms, xmlWriterSettings);
xmlWriter.WriteStartDocument();
//xmlWriter.WriteComment("DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" " + "\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\"");
xmlWriter.WriteDocType("plist", "-//Apple Computer//DTD PLIST 1.0//EN", "http://www.apple.com/DTDs/PropertyList-1.0.dtd", null);
xmlWriter.WriteStartElement("plist");
xmlWriter.WriteAttributeString("version", "1.0");
Compose(value, xmlWriter);
xmlWriter.WriteEndElement();
xmlWriter.WriteEndDocument();
xmlWriter.Flush();
xmlWriter.Close();
return Encoding.UTF8.GetString(ms.ToArray());
}
public static void WriteBinary(object value, string path)
{
using var writer = new BinaryWriter(new FileStream(path, FileMode.Create));
writer.Write(WriteBinary(value));
}
public static void WriteBinary(object value, Stream stream)
{
using var writer = new BinaryWriter(stream);
writer.Write(WriteBinary(value));
}
public static byte[] WriteBinary(object value)
{
offsetTable.Clear();
objectTable.Clear();
refCount = 0;
objRefSize = 0;
offsetByteSize = 0;
offsetTableOffset = 0;
//Do not count the root node, subtract by 1
int totalRefs = CountObject(value) - 1;
refCount = totalRefs;
objRefSize = RegulateNullBytes(BitConverter.GetBytes(refCount)).Length;
ComposeBinary(value);
WriteBinaryString("bplist00", false);
offsetTableOffset = objectTable.Count;
offsetTable.Add(objectTable.Count - 8);
offsetByteSize = RegulateNullBytes(BitConverter.GetBytes(offsetTable[offsetTable.Count - 1])).Length;
List offsetBytes = [];
offsetTable.Reverse();
for (int i = 0; i < offsetTable.Count; i++)
{
offsetTable[i] = objectTable.Count - offsetTable[i];
byte[] buffer = RegulateNullBytes(BitConverter.GetBytes(offsetTable[i]), offsetByteSize);
Array.Reverse(buffer);
offsetBytes.AddRange(buffer);
}
objectTable.AddRange(offsetBytes);
objectTable.AddRange(new byte[6]);
objectTable.Add(Convert.ToByte(offsetByteSize));
objectTable.Add(Convert.ToByte(objRefSize));
var a = BitConverter.GetBytes((long)totalRefs + 1);
Array.Reverse(a);
objectTable.AddRange(a);
objectTable.AddRange(BitConverter.GetBytes((long)0));
a = BitConverter.GetBytes(offsetTableOffset);
Array.Reverse(a);
objectTable.AddRange(a);
return [.. objectTable];
}
private static object ReadXml(XmlDocument xml)
{
XmlNode rootNode = xml.DocumentElement.ChildNodes[0];
return Parse(rootNode);
}
private static object ReadBinary(byte[] data)
{
offsetTable.Clear();
objectTable.Clear();
refCount = 0;
objRefSize = 0;
offsetByteSize = 0;
offsetTableOffset = 0;
List bList = [.. data];
List trailer = bList.GetRange(bList.Count - 32, 32);
ParseTrailer(trailer);
objectTable = bList.GetRange(0, (int)offsetTableOffset);
List offsetTableBytes = bList.GetRange((int)offsetTableOffset, bList.Count - (int)offsetTableOffset - 32);
ParseOffsetTable(offsetTableBytes);
return ParseBinary(0);
}
private static Dictionary ParseDictionary(XmlNode node)
{
XmlNodeList children = node.ChildNodes;
if (children.Count % 2 != 0)
{
throw new DataMisalignedException("Dictionary elements must have an even number of child nodes");
}
Dictionary dict = [];
for (int i = 0; i < children.Count; i += 2)
{
XmlNode keynode = children[i];
XmlNode valnode = children[i + 1];
if (keynode.Name != "key")
{
throw new ApplicationException("expected a key node");
}
object result = Parse(valnode);
if (result != null)
{
dict.Add(keynode.InnerText, result);
}
}
//dict.Add("$Proxy", node);
return dict;
}
private static List