994 lines
30 KiB
C#
994 lines
30 KiB
C#
/**
|
|
* Copyright Epic Games, Inc. All Rights Reserved.
|
|
*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Net;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading;
|
|
using System.Xml;
|
|
using System.Security.Cryptography;
|
|
using Org.BouncyCastle.Crypto;
|
|
using Org.BouncyCastle.Security;
|
|
using Org.BouncyCastle.Crypto.Parameters;
|
|
using Org.BouncyCastle.Math;
|
|
using System.Security.Cryptography.X509Certificates;
|
|
using Org.BouncyCastle.X509;
|
|
using Org.BouncyCastle.Pkcs;
|
|
using System.Collections;
|
|
|
|
namespace iPhonePackager
|
|
{
|
|
public class Utilities
|
|
{
|
|
/// <summary>
|
|
/// Reads a string from a fixed-length ASCII array
|
|
/// </summary>
|
|
public static string ReadFixedASCII(BinaryReader SR, int Length)
|
|
{
|
|
byte[] StringAsBytes = SR.ReadBytes(Length);
|
|
|
|
int StringLength = 0;
|
|
while ((StringLength < Length) && (StringAsBytes[StringLength] != 0))
|
|
{
|
|
StringLength++;
|
|
}
|
|
|
|
return Encoding.ASCII.GetString(StringAsBytes, 0, StringLength);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a string as a fixed-length ASCII array
|
|
/// </summary>
|
|
public static void WriteFixedASCII(BinaryWriter SW, string WriteOut, int Length)
|
|
{
|
|
// Encode back to ASCII
|
|
byte[] StringAsBytesUnsized = Encoding.ASCII.GetBytes(WriteOut);
|
|
int ValidByteCount = Math.Min(StringAsBytesUnsized.Length, Length);
|
|
|
|
// Size it out to a fixed length buffer
|
|
byte[] StringAsBytes = new byte[Length];
|
|
Array.Copy(StringAsBytesUnsized, StringAsBytes, ValidByteCount);
|
|
while (ValidByteCount < Length)
|
|
{
|
|
StringAsBytes[ValidByteCount] = 0;
|
|
++ValidByteCount;
|
|
}
|
|
|
|
SW.Write(StringAsBytes);
|
|
}
|
|
|
|
public static byte[] CreateASCIIZ(string WriteOut)
|
|
{
|
|
// Encode back to ASCII
|
|
byte[] StringAsBytesUnsized = Encoding.ASCII.GetBytes(WriteOut);
|
|
int ValidByteCount = StringAsBytesUnsized.Length;
|
|
int Length = ValidByteCount + 1;
|
|
|
|
// Size it out to a fixed length buffer
|
|
byte[] StringAsBytes = new byte[Length];
|
|
Array.Copy(StringAsBytesUnsized, StringAsBytes, ValidByteCount);
|
|
while (ValidByteCount < Length)
|
|
{
|
|
StringAsBytes[ValidByteCount] = 0;
|
|
++ValidByteCount;
|
|
}
|
|
|
|
return StringAsBytes;
|
|
}
|
|
|
|
public static void VerifyStreamPosition(BinaryWriter SW, long StartingStreamPosition, long JustWroteCount)
|
|
{
|
|
long ExpectedPosition = StartingStreamPosition + JustWroteCount;
|
|
if (SW.BaseStream.Position != ExpectedPosition)
|
|
{
|
|
throw new InvalidDataException(String.Format("Stream offset is not as expected, wrote too {0} data!", (SW.BaseStream.Position < ExpectedPosition) ? "little" : "much"));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reads the specified environment variable
|
|
*
|
|
* @param VarName the environment variable to read
|
|
* @param bDefault the default value to use if missing
|
|
* @return the value of the environment variable if found and the default value if missing
|
|
*/
|
|
public static bool GetEnvironmentVariable(string VarName, bool bDefault)
|
|
{
|
|
string Value = Environment.GetEnvironmentVariable(VarName);
|
|
if (Value != null)
|
|
{
|
|
// Convert the string to its boolean value
|
|
return Convert.ToBoolean(Value);
|
|
}
|
|
return bDefault;
|
|
}
|
|
|
|
/**
|
|
* Reads the specified environment variable
|
|
*
|
|
* @param VarName the environment variable to read
|
|
* @param Default the default value to use if missing
|
|
* @return the value of the environment variable if found and the default value if missing
|
|
*/
|
|
public static string GetStringEnvironmentVariable(string VarName, string Default)
|
|
{
|
|
string Value = Environment.GetEnvironmentVariable(VarName);
|
|
if (Value != null)
|
|
{
|
|
return Value;
|
|
}
|
|
return Default;
|
|
}
|
|
|
|
/**
|
|
* Reads the specified environment variable
|
|
*
|
|
* @param VarName the environment variable to read
|
|
* @param Default the default value to use if missing
|
|
* @return the value of the environment variable if found and the default value if missing
|
|
*/
|
|
public static double GetEnvironmentVariable(string VarName, double Default)
|
|
{
|
|
string Value = Environment.GetEnvironmentVariable(VarName);
|
|
if (Value != null)
|
|
{
|
|
return Convert.ToDouble(Value);
|
|
}
|
|
return Default;
|
|
}
|
|
|
|
/**
|
|
* Reads the specified environment variable
|
|
*
|
|
* @param VarName the environment variable to read
|
|
* @param Default the default value to use if missing
|
|
* @return the value of the environment variable if found and the default value if missing
|
|
*/
|
|
public static string GetEnvironmentVariable(string VarName, string Default)
|
|
{
|
|
string Value = Environment.GetEnvironmentVariable(VarName);
|
|
if (Value != null)
|
|
{
|
|
return Value;
|
|
}
|
|
return Default;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Runs an executable with the specified argument list and waits for it to terminate, capturing the standard output and return code
|
|
/// </summary>
|
|
public static int RunExecutableAndWait(string ExeName, string ArgumentList, out string StdOutResults)
|
|
{
|
|
// Create the process
|
|
ProcessStartInfo PSI = new ProcessStartInfo(ExeName, ArgumentList);
|
|
PSI.RedirectStandardOutput = true;
|
|
PSI.UseShellExecute = false;
|
|
PSI.CreateNoWindow = true;
|
|
Process NewProcess = Process.Start(PSI);
|
|
|
|
// Wait for the process to exit and grab it's output
|
|
StdOutResults = NewProcess.StandardOutput.ReadToEnd();
|
|
NewProcess.WaitForExit();
|
|
return NewProcess.ExitCode;
|
|
}
|
|
|
|
public class PListHelper
|
|
{
|
|
public XmlDocument Doc;
|
|
|
|
bool bReadOnly = false;
|
|
|
|
public void SetReadOnly(bool bNowReadOnly)
|
|
{
|
|
bReadOnly = bNowReadOnly;
|
|
}
|
|
|
|
public PListHelper(string Source)
|
|
{
|
|
Doc = new XmlDocument();
|
|
Doc.XmlResolver = null;
|
|
Doc.LoadXml(Source);
|
|
}
|
|
|
|
public static PListHelper CreateFromFile(string Filename)
|
|
{
|
|
byte[] RawPList = File.ReadAllBytes(Filename);
|
|
return new PListHelper(Encoding.UTF8.GetString(RawPList));
|
|
}
|
|
|
|
public void SaveToFile(string Filename)
|
|
{
|
|
File.WriteAllText(Filename, SaveToString(), Encoding.UTF8);
|
|
}
|
|
|
|
public PListHelper()
|
|
{
|
|
string EmptyFileText =
|
|
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
|
|
"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n" +
|
|
"<plist version=\"1.0\">\n" +
|
|
"<dict>\n" +
|
|
"</dict>\n" +
|
|
"</plist>\n";
|
|
|
|
Doc = new XmlDocument();
|
|
Doc.XmlResolver = null;
|
|
Doc.LoadXml(EmptyFileText);
|
|
}
|
|
|
|
public XmlElement ConvertValueToPListFormat(object Value)
|
|
{
|
|
XmlElement ValueElement = null;
|
|
if (Value is string)
|
|
{
|
|
ValueElement = Doc.CreateElement("string");
|
|
ValueElement.InnerText = Value as string;
|
|
}
|
|
else if (Value is Dictionary<string, object>)
|
|
{
|
|
ValueElement = Doc.CreateElement("dict");
|
|
foreach (var KVP in Value as Dictionary<string, object>)
|
|
{
|
|
AddKeyValuePair(ValueElement, KVP.Key, KVP.Value);
|
|
}
|
|
}
|
|
else if (Value is Utilities.PListHelper)
|
|
{
|
|
Utilities.PListHelper PList = Value as Utilities.PListHelper;
|
|
|
|
ValueElement = Doc.CreateElement("dict");
|
|
|
|
XmlNode SourceDictionaryNode = PList.Doc.DocumentElement.SelectSingleNode("/plist/dict");
|
|
foreach (XmlNode TheirChild in SourceDictionaryNode)
|
|
{
|
|
ValueElement.AppendChild(Doc.ImportNode(TheirChild, true));
|
|
}
|
|
}
|
|
else if (Value is Array)
|
|
{
|
|
if (Value is byte[])
|
|
{
|
|
ValueElement = Doc.CreateElement("data");
|
|
ValueElement.InnerText = Convert.ToBase64String(Value as byte[]);
|
|
}
|
|
else
|
|
{
|
|
ValueElement = Doc.CreateElement("array");
|
|
foreach (var A in Value as Array)
|
|
{
|
|
ValueElement.AppendChild(ConvertValueToPListFormat(A));
|
|
}
|
|
}
|
|
}
|
|
else if (Value is IList)
|
|
{
|
|
ValueElement = Doc.CreateElement("array");
|
|
foreach (var A in Value as IList)
|
|
{
|
|
ValueElement.AppendChild(ConvertValueToPListFormat(A));
|
|
}
|
|
}
|
|
else if (Value is bool)
|
|
{
|
|
ValueElement = Doc.CreateElement(((bool)Value) ? "true" : "false");
|
|
}
|
|
else if (Value is double)
|
|
{
|
|
ValueElement = Doc.CreateElement("real");
|
|
ValueElement.InnerText = ((double)Value).ToString();
|
|
}
|
|
else if (Value is int)
|
|
{
|
|
ValueElement = Doc.CreateElement("integer");
|
|
ValueElement.InnerText = ((int)Value).ToString();
|
|
}
|
|
else
|
|
{
|
|
throw new InvalidDataException(String.Format("Object '{0}' is in an unknown type that cannot be converted to PList format", Value));
|
|
}
|
|
|
|
return ValueElement;
|
|
|
|
}
|
|
|
|
public void AddKeyValuePair(XmlNode DictRoot, string KeyName, object Value)
|
|
{
|
|
if (bReadOnly)
|
|
{
|
|
throw new AccessViolationException("PList has been set to read only and may not be modified");
|
|
}
|
|
|
|
XmlElement KeyElement = Doc.CreateElement("key");
|
|
KeyElement.InnerText = KeyName;
|
|
|
|
DictRoot.AppendChild(KeyElement);
|
|
DictRoot.AppendChild(ConvertValueToPListFormat(Value));
|
|
}
|
|
|
|
public void AddKeyValuePair(string KeyName, object Value)
|
|
{
|
|
XmlNode DictRoot = Doc.DocumentElement.SelectSingleNode("/plist/dict");
|
|
|
|
AddKeyValuePair(DictRoot, KeyName, Value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clones a dictionary from an existing .plist into a new one. Root should point to the dict key in the source plist.
|
|
/// </summary>
|
|
public static PListHelper CloneDictionaryRootedAt(XmlNode Root)
|
|
{
|
|
// Create a new empty dictionary
|
|
PListHelper Result = new PListHelper();
|
|
|
|
// Copy all of the entries in the source dictionary into the new one
|
|
XmlNode NewDictRoot = Result.Doc.DocumentElement.SelectSingleNode("/plist/dict");
|
|
foreach (XmlNode TheirChild in Root)
|
|
{
|
|
NewDictRoot.AppendChild(Result.Doc.ImportNode(TheirChild, true));
|
|
}
|
|
|
|
return Result;
|
|
}
|
|
|
|
public bool GetString(string Key, out string Value)
|
|
{
|
|
string PathToValue = String.Format("/plist/dict/key[.='{0}']/following-sibling::string[1]", Key);
|
|
|
|
XmlNode ValueNode = Doc.DocumentElement.SelectSingleNode(PathToValue);
|
|
if (ValueNode == null)
|
|
{
|
|
Value = "";
|
|
return false;
|
|
}
|
|
|
|
Value = ValueNode.InnerText;
|
|
return true;
|
|
}
|
|
|
|
public bool GetDate(string Key, out string Value)
|
|
{
|
|
string PathToValue = String.Format("/plist/dict/key[.='{0}']/following-sibling::date[1]", Key);
|
|
|
|
XmlNode ValueNode = Doc.DocumentElement.SelectSingleNode(PathToValue);
|
|
if (ValueNode == null)
|
|
{
|
|
Value = "";
|
|
return false;
|
|
}
|
|
|
|
Value = ValueNode.InnerText;
|
|
return true;
|
|
}
|
|
|
|
public bool GetBool(string Key)
|
|
{
|
|
string PathToValue = String.Format("/plist/dict/key[.='{0}']/following-sibling::node()", Key);
|
|
|
|
XmlNode ValueNode = Doc.DocumentElement.SelectSingleNode(PathToValue);
|
|
if (ValueNode == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return ValueNode.Name == "true";
|
|
}
|
|
|
|
public delegate void ProcessOneNodeEvent(XmlNode ValueNode);
|
|
|
|
public void ProcessValueForKey(string Key, string ExpectedValueType, ProcessOneNodeEvent ValueHandler)
|
|
{
|
|
string PathToValue = String.Format("/plist/dict/key[.='{0}']/following-sibling::{1}[1]", Key, ExpectedValueType);
|
|
|
|
XmlNode ValueNode = Doc.DocumentElement.SelectSingleNode(PathToValue);
|
|
if (ValueNode != null)
|
|
{
|
|
ValueHandler(ValueNode);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Merge two plists together. Whenever both have the same key, the value in the dominant source list wins.
|
|
/// This is special purpose code, and only handles things inside of the <dict> tag
|
|
/// </summary>
|
|
public void MergePlistIn(string DominantPlist, HashSet<string> WeakKeysToKeep=null)
|
|
{
|
|
if (bReadOnly)
|
|
{
|
|
throw new AccessViolationException("PList has been set to read only and may not be modified");
|
|
}
|
|
|
|
XmlDocument Dominant = new XmlDocument();
|
|
Dominant.XmlResolver = null;
|
|
Dominant.LoadXml(DominantPlist);
|
|
|
|
XmlNode DictionaryNode = Doc.DocumentElement.SelectSingleNode("/plist/dict");
|
|
|
|
// Merge any key-value pairs in the strong .plist into the weak .plist
|
|
XmlNodeList StrongKeys = Dominant.DocumentElement.SelectNodes("/plist/dict/key");
|
|
foreach (XmlNode StrongKeyNode in StrongKeys)
|
|
{
|
|
string StrongKey = StrongKeyNode.InnerText;
|
|
|
|
XmlNode WeakNode = Doc.DocumentElement.SelectSingleNode(String.Format("/plist/dict/key[.='{0}']", StrongKey));
|
|
if (WeakNode == null)
|
|
{
|
|
// Doesn't exist in dominant plist, inject key-value pair
|
|
DictionaryNode.AppendChild(Doc.ImportNode(StrongKeyNode, true));
|
|
DictionaryNode.AppendChild(Doc.ImportNode(StrongKeyNode.NextSibling, true));
|
|
}
|
|
// don't overwrite values we want to keep
|
|
else if (WeakKeysToKeep == null || !WeakKeysToKeep.Contains(StrongKey))
|
|
{
|
|
// Remove the existing value node from the weak file
|
|
WeakNode.ParentNode.RemoveChild(WeakNode.NextSibling);
|
|
|
|
// Insert a clone of the dominant value node
|
|
WeakNode.ParentNode.InsertAfter(Doc.ImportNode(StrongKeyNode.NextSibling, true), WeakNode);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns each of the entries in the value tag of type array for a given key
|
|
/// If the key is missing, an empty array is returned.
|
|
/// Only entries of a given type within the array are returned.
|
|
/// </summary>
|
|
public List<string> GetArray(string Key, string EntryType)
|
|
{
|
|
List<string> Result = new List<string>();
|
|
|
|
ProcessValueForKey(Key, "array",
|
|
delegate(XmlNode ValueNode)
|
|
{
|
|
foreach (XmlNode ChildNode in ValueNode.ChildNodes)
|
|
{
|
|
if (EntryType == ChildNode.Name)
|
|
{
|
|
string Value = ChildNode.InnerText;
|
|
Result.Add(Value);
|
|
}
|
|
}
|
|
});
|
|
|
|
return Result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the key exists (and has a value) and false otherwise
|
|
/// </summary>
|
|
public bool HasKey(string KeyName)
|
|
{
|
|
string PathToKey = String.Format("/plist/dict/key[.='{0}']", KeyName);
|
|
|
|
XmlNode KeyNode = Doc.DocumentElement.SelectSingleNode(PathToKey);
|
|
return (KeyNode != null);
|
|
}
|
|
|
|
public void RemoveKeyValue(string KeyName)
|
|
{
|
|
if (bReadOnly)
|
|
{
|
|
throw new AccessViolationException("PList has been set to read only and may not be modified");
|
|
}
|
|
|
|
XmlNode DictionaryNode = Doc.DocumentElement.SelectSingleNode("/plist/dict");
|
|
|
|
string PathToKey = String.Format("/plist/dict/key[.='{0}']", KeyName);
|
|
XmlNode KeyNode = Doc.DocumentElement.SelectSingleNode(PathToKey);
|
|
if (KeyNode != null && KeyNode.ParentNode != null)
|
|
{
|
|
XmlNode ValueNode = KeyNode.NextSibling;
|
|
//remove value
|
|
if (ValueNode != null)
|
|
{
|
|
ValueNode.RemoveAll();
|
|
ValueNode.ParentNode.RemoveChild(ValueNode);
|
|
}
|
|
//remove key
|
|
KeyNode.RemoveAll();
|
|
KeyNode.ParentNode.RemoveChild(KeyNode);
|
|
}
|
|
}
|
|
|
|
public void SetValueForKey(string KeyName, object Value)
|
|
{
|
|
if (bReadOnly)
|
|
{
|
|
throw new AccessViolationException("PList has been set to read only and may not be modified");
|
|
}
|
|
|
|
XmlNode DictionaryNode = Doc.DocumentElement.SelectSingleNode("/plist/dict");
|
|
|
|
string PathToKey = String.Format("/plist/dict/key[.='{0}']", KeyName);
|
|
XmlNode KeyNode = Doc.DocumentElement.SelectSingleNode(PathToKey);
|
|
|
|
XmlNode ValueNode = null;
|
|
if (KeyNode != null)
|
|
{
|
|
ValueNode = KeyNode.NextSibling;
|
|
}
|
|
|
|
if (ValueNode == null)
|
|
{
|
|
KeyNode = Doc.CreateNode(XmlNodeType.Element, "key", null);
|
|
KeyNode.InnerText = KeyName;
|
|
|
|
ValueNode = ConvertValueToPListFormat(Value);
|
|
|
|
DictionaryNode.AppendChild(KeyNode);
|
|
DictionaryNode.AppendChild(ValueNode);
|
|
}
|
|
else
|
|
{
|
|
// Remove the existing value and create a new one
|
|
ValueNode.ParentNode.RemoveChild(ValueNode);
|
|
ValueNode = ConvertValueToPListFormat(Value);
|
|
|
|
// Insert the value after the key
|
|
DictionaryNode.InsertAfter(ValueNode, KeyNode);
|
|
}
|
|
}
|
|
|
|
public void SetString(string Key, string Value)
|
|
{
|
|
SetValueForKey(Key, Value);
|
|
}
|
|
|
|
public string SaveToString()
|
|
{
|
|
// Convert the XML back to text in the same style as the original .plist
|
|
StringBuilder TextOut = new StringBuilder();
|
|
|
|
// Work around the fact it outputs the wrong encoding by default (and set some other settings to get something similar to the input file)
|
|
TextOut.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
|
|
XmlWriterSettings Settings = new XmlWriterSettings();
|
|
Settings.Indent = true;
|
|
Settings.IndentChars = "\t";
|
|
Settings.NewLineChars = "\n";
|
|
Settings.NewLineHandling = NewLineHandling.Replace;
|
|
Settings.OmitXmlDeclaration = true;
|
|
Settings.Encoding = new UTF8Encoding(false);
|
|
|
|
// Work around the fact that it embeds an empty declaration list to the document type which codesign dislikes...
|
|
// Replacing InternalSubset with null if it's empty. The property is readonly, so we have to reconstruct it entirely
|
|
Doc.ReplaceChild(Doc.CreateDocumentType(
|
|
Doc.DocumentType.Name,
|
|
Doc.DocumentType.PublicId,
|
|
Doc.DocumentType.SystemId,
|
|
String.IsNullOrEmpty(Doc.DocumentType.InternalSubset) ? null : Doc.DocumentType.InternalSubset),
|
|
Doc.DocumentType);
|
|
|
|
XmlWriter Writer = XmlWriter.Create(TextOut, Settings);
|
|
|
|
Doc.Save(Writer);
|
|
|
|
// Remove the space from any standalone XML elements because the iOS parser does not handle them
|
|
return Regex.Replace(TextOut.ToString(), @"<(?<tag>\S+) />", "<${tag}/>");
|
|
}
|
|
}
|
|
|
|
static public string GetStringFromPList(string KeyName)
|
|
{
|
|
// Open the .plist and read out the specified key
|
|
string PListAsString;
|
|
if (!Utilities.GetSourcePList(out PListAsString))
|
|
{
|
|
Program.Error("Failed to find source PList");
|
|
Program.ReturnCode = (int)ErrorCodes.Error_InfoPListNotFound;
|
|
return "(unknown)";
|
|
}
|
|
|
|
PListHelper Helper = new PListHelper(PListAsString);
|
|
|
|
string Result;
|
|
if (Helper.GetString(KeyName, out Result))
|
|
{
|
|
return Result;
|
|
}
|
|
else
|
|
{
|
|
Program.Error("Failed to find a value for {0} in PList", KeyName);
|
|
Program.ReturnCode = (int)ErrorCodes.Error_KeyNotFoundInPList;
|
|
return "(unknown)";
|
|
}
|
|
}
|
|
|
|
static public string GetPrecompileSourcePListFilename()
|
|
{
|
|
// check for one in the project directory
|
|
string SourceName = FileOperations.FindPrefixedFile(Config.IntermediateDirectory, Program.GameName + "-Info.plist");
|
|
|
|
if (!File.Exists(SourceName))
|
|
{
|
|
// Check for a premade one
|
|
SourceName = FileOperations.FindPrefixedFile(Config.BuildDirectory, Program.GameName + "-Info.plist");
|
|
|
|
if (!File.Exists(SourceName))
|
|
{
|
|
// fallback to the shared one
|
|
SourceName = FileOperations.FindPrefixedFile(Config.EngineBuildDirectory, "UnrealGame-Info.plist");
|
|
|
|
if (!File.Exists(SourceName))
|
|
{
|
|
Program.Log("Failed to find " + Program.GameName + "-Info.plist. Please create new .plist or copy a base .plist from a provided game sample.");
|
|
}
|
|
}
|
|
}
|
|
|
|
return SourceName;
|
|
}
|
|
|
|
/**
|
|
* Handle grabbing the initial plist
|
|
*/
|
|
static public bool GetSourcePList(out string PListSource)
|
|
{
|
|
// Check for a premade one
|
|
string SourceName = GetPrecompileSourcePListFilename();
|
|
|
|
if (File.Exists(SourceName))
|
|
{
|
|
Program.Log(" ... reading source .plist: " + SourceName);
|
|
PListSource = File.ReadAllText(SourceName);
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
Program.Error(" ... failed to locate the source .plist file");
|
|
Program.ReturnCode = (int)ErrorCodes.Error_KeyNotFoundInPList;
|
|
PListSource = "";
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
class VersionUtilities
|
|
{
|
|
static string RunningVersionFilename
|
|
{
|
|
get { return Path.Combine(Config.BuildDirectory, Program.GameName + ".PackageVersionCounter"); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads the GameName.PackageVersionCounter from disk and bumps the minor version number in it
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public static string ReadRunningVersion()
|
|
{
|
|
string CurrentVersion = "0.0";
|
|
if (File.Exists(RunningVersionFilename))
|
|
{
|
|
CurrentVersion = File.ReadAllText(RunningVersionFilename);
|
|
}
|
|
|
|
return CurrentVersion;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pulls apart a version string of one of the two following formats:
|
|
/// "7301.15 11-01 10:28" (Major.Minor Date Time)
|
|
/// "7486.0" (Major.Minor)
|
|
/// </summary>
|
|
/// <param name="CFBundleVersion"></param>
|
|
/// <param name="VersionMajor"></param>
|
|
/// <param name="VersionMinor"></param>
|
|
/// <param name="TimeStamp"></param>
|
|
public static void PullApartVersion(string CFBundleVersion, out int VersionMajor, out int VersionMinor, out string TimeStamp)
|
|
{
|
|
// Expecting source to be like "7301.15 11-01 10:28" or "7486.0"
|
|
string[] Parts = CFBundleVersion.Split(new char[] { ' ' });
|
|
|
|
// Parse the version string
|
|
string[] VersionParts = Parts[0].Split(new char[] { '.' });
|
|
|
|
if (!int.TryParse(VersionParts[0], out VersionMajor))
|
|
{
|
|
VersionMajor = 0;
|
|
}
|
|
|
|
if ((VersionParts.Length < 2) || (!int.TryParse(VersionParts[1], out VersionMinor)))
|
|
{
|
|
VersionMinor = 0;
|
|
}
|
|
|
|
TimeStamp = "";
|
|
if (Parts.Length > 1)
|
|
{
|
|
TimeStamp = String.Join(" ", Parts, 1, Parts.Length - 1);
|
|
}
|
|
}
|
|
|
|
public static string ConstructVersion(int MajorVersion, int MinorVersion)
|
|
{
|
|
return String.Format("{0}.{1}", MajorVersion, MinorVersion);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses the version string (expected to be of the form major.minor or major)
|
|
/// Also parses the major.minor from the running version file and increments it's minor by 1.
|
|
///
|
|
/// If the running version major matches and the running version minor is newer, then the bundle version is updated.
|
|
///
|
|
/// In either case, the running version is set to the current bundle version number and written back out.
|
|
/// </summary>
|
|
/// <returns>The (possibly updated) bundle version</returns>
|
|
public static string CalculateUpdatedMinorVersionString(string CFBundleVersion)
|
|
{
|
|
// Read the running version and bump it
|
|
int RunningMajorVersion;
|
|
int RunningMinorVersion;
|
|
|
|
string DummyDate;
|
|
string RunningVersion = ReadRunningVersion();
|
|
PullApartVersion(RunningVersion, out RunningMajorVersion, out RunningMinorVersion, out DummyDate);
|
|
RunningMinorVersion++;
|
|
|
|
// Read the passed in version and bump it
|
|
int MajorVersion;
|
|
int MinorVersion;
|
|
PullApartVersion(CFBundleVersion, out MajorVersion, out MinorVersion, out DummyDate);
|
|
MinorVersion++;
|
|
|
|
// Combine them if the stub time is older
|
|
if ((RunningMajorVersion == MajorVersion) && (RunningMinorVersion > MinorVersion))
|
|
{
|
|
// A subsequent cook on the same sync, the only time that we stomp on the stub version
|
|
MinorVersion = RunningMinorVersion;
|
|
}
|
|
|
|
// Combine them together
|
|
string ResultVersionString = ConstructVersion(MajorVersion, MinorVersion);
|
|
|
|
// Update the running version file
|
|
Directory.CreateDirectory(Path.GetDirectoryName(RunningVersionFilename));
|
|
File.WriteAllText(RunningVersionFilename, ResultVersionString);
|
|
|
|
return ResultVersionString;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the minor version in the CFBundleVersion key of the specified PList if this is a new package.
|
|
/// Also updates the key EpicAppVersion with the bundle version and the current date/time (no year)
|
|
/// </summary>
|
|
public static void UpdateMinorVersion(Utilities.PListHelper PList)
|
|
{
|
|
string CFBundleVersion;
|
|
if (PList.GetString("CFBundleVersion", out CFBundleVersion))
|
|
{
|
|
string UpdatedValue = CalculateUpdatedMinorVersionString(CFBundleVersion);
|
|
Program.Log("Found CFBundleVersion string '{0}' and updated it to '{1}'", CFBundleVersion, UpdatedValue);
|
|
PList.SetString("CFBundleVersion", UpdatedValue);
|
|
}
|
|
else
|
|
{
|
|
CFBundleVersion = "0.0";
|
|
}
|
|
|
|
// Write a second key with the packaging date/time in it
|
|
string PackagingTime = DateTime.Now.ToString(@"MM-dd HH:mm");
|
|
PList.SetString("EpicAppVersion", CFBundleVersion + " " + PackagingTime);
|
|
}
|
|
}
|
|
|
|
class CryptoAdapter
|
|
{
|
|
public static AsymmetricCipherKeyPair GenerateBouncyKeyPair()
|
|
{
|
|
// Construct a new public/private key pair (RSA 2048 bits)
|
|
IAsymmetricCipherKeyPairGenerator KeyGen = GeneratorUtilities.GetKeyPairGenerator("RSA");
|
|
RsaKeyGenerationParameters KeyParams = new RsaKeyGenerationParameters(BigInteger.ValueOf(0x10001), new SecureRandom(), 2048, 25);
|
|
KeyGen.Init(KeyParams);
|
|
|
|
AsymmetricCipherKeyPair KeyPair = KeyGen.GenerateKeyPair();
|
|
|
|
return KeyPair;
|
|
}
|
|
|
|
|
|
public static RSACryptoServiceProvider LoadKeyPairFromPKCS12Store(string Filename)
|
|
{
|
|
Pkcs12Store Store = new Pkcs12StoreBuilder().Build();
|
|
Store.Load(File.OpenRead(Filename), new char[0]);
|
|
|
|
foreach (string Alias in Store.Aliases)
|
|
{
|
|
if (Store.IsKeyEntry(Alias))
|
|
{
|
|
Console.WriteLine("Key with alias {0} is {1}", Alias, Store.GetKey(Alias).Key);
|
|
return ConvertBouncyKeyPairToNET(Store.GetKey(Alias).Key as RsaPrivateCrtKeyParameters);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public static Org.BouncyCastle.X509.X509Certificate LoadBouncyCertFromPKCS12Store(string Filename)
|
|
{
|
|
Pkcs12Store Store = new Pkcs12StoreBuilder().Build();
|
|
Store.Load(File.OpenRead(Filename), new char[0]);
|
|
|
|
foreach (string Alias in Store.Aliases)
|
|
{
|
|
if (Store.IsCertificateEntry(Alias))
|
|
{
|
|
return Store.GetCertificate(Alias).Certificate;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public static RSACryptoServiceProvider LoadKeyPairFromDiskNET(string Filename)
|
|
{
|
|
CspParameters Setup = new CspParameters();
|
|
Setup.KeyContainerName = "MyKeyContainer";
|
|
|
|
RSACryptoServiceProvider KeyPair = new RSACryptoServiceProvider(Setup);
|
|
KeyPair.PersistKeyInCsp = true;
|
|
|
|
byte[] FileData = null;
|
|
try
|
|
{
|
|
FileData = File.ReadAllBytes(Filename);
|
|
KeyPair.FromXmlString(Encoding.UTF8.GetString(FileData));
|
|
}
|
|
catch (Exception)
|
|
{
|
|
try
|
|
{
|
|
KeyPair.ImportCspBlob(FileData);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
try
|
|
{
|
|
return LoadKeyPairFromPKCS12Store(Filename);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
return KeyPair;
|
|
}
|
|
|
|
public static AsymmetricCipherKeyPair LoadKeyPairFromDiskBouncy(string Filename)
|
|
{
|
|
RSACryptoServiceProvider KeyNET = LoadKeyPairFromDiskNET(Filename);
|
|
if (KeyNET != null)
|
|
{
|
|
return ConvertNETKeyPairToBouncy(KeyNET);
|
|
}
|
|
else
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public static AsymmetricCipherKeyPair ConvertNETKeyPairToBouncy(RSACryptoServiceProvider KeyPair)
|
|
{
|
|
return DotNetUtilities.GetKeyPair(KeyPair);
|
|
}
|
|
|
|
public static RSACryptoServiceProvider ConvertBouncyKeyPairToNET(AsymmetricCipherKeyPair KeyPair)
|
|
{
|
|
RsaPrivateCrtKeyParameters PrivateKeyInfo = KeyPair.Private as RsaPrivateCrtKeyParameters;
|
|
|
|
return ConvertBouncyKeyPairToNET(PrivateKeyInfo);
|
|
}
|
|
|
|
public static RSACryptoServiceProvider ConvertBouncyKeyPairToNET(RsaPrivateCrtKeyParameters PrivateKeyInfo)
|
|
{
|
|
CspParameters CSPParams = new CspParameters();
|
|
CSPParams.KeyContainerName = "KeyContainer";
|
|
RSACryptoServiceProvider Result = new RSACryptoServiceProvider(2048, CSPParams);
|
|
|
|
RSAParameters PrivateKeyInfoDotNET = DotNetUtilities.ToRSAParameters(PrivateKeyInfo);
|
|
Result.ImportParameters(PrivateKeyInfoDotNET);
|
|
|
|
return Result;
|
|
}
|
|
|
|
public static string GetCommonNameFromCert(X509Certificate2 Cert)
|
|
{
|
|
// Make sure we have a useful friendly name
|
|
string CommonName = "(no common name present)";
|
|
|
|
string FullName = Cert.SubjectName.Name;
|
|
char[] SplitChars = { ',' };
|
|
string[] NameParts = FullName.Split(SplitChars);
|
|
|
|
foreach (string Part in NameParts)
|
|
{
|
|
string CleanPart = Part.Trim();
|
|
if (CleanPart.StartsWith("CN="))
|
|
{
|
|
CommonName = CleanPart.Substring(3);
|
|
}
|
|
}
|
|
|
|
return CommonName;
|
|
}
|
|
|
|
public static string GetFriendlyNameFromCert(X509Certificate2 Cert)
|
|
{
|
|
if (Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX)
|
|
{
|
|
return GetCommonNameFromCert(Cert);
|
|
}
|
|
else
|
|
{
|
|
// Make sure we have a useful friendly name
|
|
string FriendlyName = Cert.FriendlyName;
|
|
if ((FriendlyName == "") || (FriendlyName == null))
|
|
{
|
|
FriendlyName = GetCommonNameFromCert(Cert);
|
|
}
|
|
|
|
return FriendlyName;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Merges a certificate and private key into a single combined certificate
|
|
/// </summary>
|
|
public static X509Certificate2 CombineKeyAndCert(string CertificateFilename, string KeyFilename)
|
|
{
|
|
// Load the certificate
|
|
string CertificatePassword = "";
|
|
X509Certificate2 Cert = new X509Certificate2(CertificateFilename, CertificatePassword, X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet);
|
|
|
|
// Make sure we have a useful friendly name
|
|
string FriendlyName = GetFriendlyNameFromCert(Cert);
|
|
|
|
// Create a PKCS#12 store with both the certificate and the private key in it
|
|
Pkcs12Store Store = new Pkcs12StoreBuilder().Build();
|
|
|
|
X509CertificateEntry[] CertChain = new X509CertificateEntry[1];
|
|
Org.BouncyCastle.X509.X509Certificate BouncyCert = DotNetUtilities.FromX509Certificate(Cert);
|
|
CertChain[0] = new X509CertificateEntry(BouncyCert);
|
|
|
|
AsymmetricCipherKeyPair KeyPair = LoadKeyPairFromDiskBouncy(KeyFilename);
|
|
if (KeyPair == null || KeyPair.Private == null)
|
|
{
|
|
throw new InvalidDataException("The key pair provided does contain a private key. Make sure you provide the same key pair that was used to generate the original certificate signing request");
|
|
}
|
|
|
|
Store.SetKeyEntry(FriendlyName, new AsymmetricKeyEntry(KeyPair.Private), CertChain);
|
|
|
|
// Verify the public key from the key pair matches the certificate's public key
|
|
AsymmetricKeyParameter CertPublicKey = BouncyCert.GetPublicKey();
|
|
if (!(KeyPair.Public as RsaKeyParameters).Equals(CertPublicKey as RsaKeyParameters))
|
|
{
|
|
throw new InvalidDataException("The key pair provided does not match the certificate. Make sure you provide the same key pair that was used to generate the original certificate signing request");
|
|
}
|
|
|
|
// Export the merged cert as a .p12
|
|
string TempFileName = Path.GetTempFileName();
|
|
|
|
string ReexportedPassword = "password";
|
|
|
|
Stream OutStream = File.OpenWrite(TempFileName);
|
|
Store.Save(OutStream, ReexportedPassword.ToCharArray(), new Org.BouncyCastle.Security.SecureRandom());
|
|
OutStream.Close();
|
|
|
|
// Load it back in and delete the temporary file
|
|
X509Certificate2 Result = new X509Certificate2(TempFileName, ReexportedPassword, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.MachineKeySet);
|
|
FileOperations.DeleteFile(TempFileName);
|
|
return Result;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|