remote_ios_build_3

in MacOS, Tutorials, UE4, Windows

Remote iOS build on UE4 (Part 3)

We are going to create a tool to test our mobileprovision file before setup our environment, a very simple program to check that the file can be read properly and display some of their entries

Creating mobile provision files
Remote iOS project set-up
Mobileprovision tester

Mobileprovision file format

Mobileprovision file is used by Apple Xcode for creating iPhone apps, and contains a provisioning profile, which allows an app to be uploaded to a limited number of iPhones or iPads while it is still in development.

Basically this information is stored in a XML format, we can open this file with a text editor and see this XML section after some header info.

mobileprovision_header

We need to extract this XML section before be able to read each entry of the plist. The provision data is stored as PKCS7-signed file in ASN.1 BER format so we need to do a class to read this ASN format before implement the XML reader. Here we have class to parser ASN.1 BER format:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace MobileProvisionReader
{
	/// <summary>
	/// Parser for ASN.1 BER formats (eg. iOS mobile provisions)
	/// </summary>
	static class Asn
	{
		public enum FieldTag
		{
			EOC = 0,
			BOOLEAN = 1,
			INTEGER = 2,
			OCTET_STRING = 4,
			NULL = 5,
			OBJECT_IDENTIFIER = 6,
			SEQUENCE = 16,
			SET = 17,
			PRINTABLE_STRING = 19,
		}

		public struct FieldInfo
		{
			public int TagClass;
			public bool bPrimitive;
			public FieldTag Tag;
			public int Length;
		}

		public static class ObjectIdentifier
		{
			public static readonly int[] CountryName = new int[] { 2, 5, 4, 6 };
			public static readonly int[] OrganizationName = new int[] { 2, 5, 4, 10 };
			public static readonly int[] Pkcs7_SignedData = new int[] { 1, 2, 840, 113549, 1, 7, 2 };
			public static readonly int[] Pkcs7_Data = new int[] { 1, 2, 840, 113549, 1, 7, 1 };
		}

		static readonly List<KeyValuePair<int[], string>> KnownObjectIdentifiers = new List<KeyValuePair<int[], string>>
		{
			new KeyValuePair<int[], string>(ObjectIdentifier.CountryName, "CountryName"),
			new KeyValuePair<int[], string>(ObjectIdentifier.OrganizationName, "OrganizationName"),
			new KeyValuePair<int[], string>(ObjectIdentifier.Pkcs7_SignedData, "Pkcs7-SignedData"),
			new KeyValuePair<int[], string>(ObjectIdentifier.Pkcs7_Data, "Pkcs7-Data"),
		};

		public static FieldInfo ReadField(BinaryReader Reader)
		{
			// Read the type and length
			int Type = Reader.ReadByte();
			int Length = ReadLength(Reader);

			// Unpack the type
			FieldInfo Field = new FieldInfo();
			Field.TagClass = (Type >> 6);
			Field.bPrimitive = (Type & 0x20) == 0;
			Field.Tag = (FieldTag)(Type & 0x1f);
			Field.Length = Length;
			return Field;
		}

		public static void SkipValue(BinaryReader Reader, FieldInfo Field)
		{
			if (Field.bPrimitive)
			{
				Reader.BaseStream.Seek(Field.Length, SeekOrigin.Current);
			}
		}

		public static int ReadInteger(BinaryReader Reader, int Length)
		{
			int Value = 0;
			for (int Idx = 0; Idx < Length; Idx++)
			{
				Value = (Value << 8) | (int)Reader.ReadByte();
			}
			return Value;
		}

		public static int ReadLength(BinaryReader Reader)
		{
			int Count = (int)Reader.ReadByte();
			if (Count <= 0x7f)
			{
				return Count;
			}
			else
			{
				return ReadInteger(Reader, Count & 0x7f);
			}
		}

		public static int[] ReadObjectIdentifier(BinaryReader Reader, int Length)
		{
			byte[] Data = Reader.ReadBytes(Length);

			List<int> Values = new List<int>();
			Values.Add((int)Data[0] / 40);
			Values.Add((int)Data[0] % 40);
			for (int Idx = 1; Idx < Data.Length; Idx++)
			{
				int Value = (int)Data[Idx] & 0x7f;
				while (((int)Data[Idx] & 0x80) != 0)
				{
					Value = (Value << 7) | ((int)Data[++Idx] & 0x7f);
				}
				Values.Add(Value);
			}
			return Values.ToArray();
		}

		public static string GetObjectIdentifierName(int[] Values)
		{
			string Description;
			if (!TryGetObjectIdentifierName(Values, out Description))
			{
				Description = String.Format("Unknown ({0})", String.Join(", ", Values.Select(x => x.ToString())));
			}
			return Description;
		}

		public static bool TryGetObjectIdentifierName(int[] Values, out string Name)
		{
			foreach (KeyValuePair<int[], string> KnownObjectIdentifier in KnownObjectIdentifiers)
			{
				if (Enumerable.SequenceEqual(Values, KnownObjectIdentifier.Key))
				{
					Name = KnownObjectIdentifier.Value;
					return true;
				}
			}

			Name = null;
			return false;
		}

		public static void Dump(BinaryReader Reader, string Indent)
		{
			FieldInfo Field = ReadField(Reader);

			// If it's a primitive type, unpack the value
			string DescriptionSuffix = "";
			if (Field.bPrimitive)
			{
				if (Field.Length > 0)
				{
					string Description;
					switch (Field.Tag)
					{
						case FieldTag.INTEGER:
							Description = String.Format("{0}", ReadInteger(Reader, Field.Length));
							break;
						case FieldTag.OBJECT_IDENTIFIER:
							Description = GetObjectIdentifierName(ReadObjectIdentifier(Reader, Field.Length));
							break;
						case FieldTag.PRINTABLE_STRING:
							Description = Encoding.ASCII.GetString(Reader.ReadBytes(Field.Length));
							break;
						default:
							Reader.ReadBytes(Field.Length);
							Description = "unknown";
							break;
					}
					DescriptionSuffix = String.Format(" = {0}", Description);
				}
			}

			// Print the contents/name of this element
			Console.WriteLine("{0}{1}{2}", Indent, Field.Tag, DescriptionSuffix);

			// If it's not a primitive, print the contents
			if (!Field.bPrimitive)
			{
				long EndOffset = Reader.BaseStream.Position + Field.Length;
				while (Reader.BaseStream.Position < EndOffset)
				{
					Dump(Reader, Indent + "  ");
				}
			}
		}
	}
}

Now we can make the XML extractor of the provision file using the previous class to parser the file.

public static XmlDocument ReadXml(string Location)
{
	// Provision data is stored as PKCS7-signed file in ASN.1 BER format
	using (BinaryReader Reader = new BinaryReader(File.Open(Location, FileMode.Open, FileAccess.Read)))
	{
		long Length = Reader.BaseStream.Length;
		while (Reader.BaseStream.Position < Length)
		{
			Asn.FieldInfo Field = Asn.ReadField(Reader);
			if (Field.Tag == Asn.FieldTag.OBJECT_IDENTIFIER)
			{
				int[] Identifier = Asn.ReadObjectIdentifier(Reader, Field.Length);
				if (Enumerable.SequenceEqual(Identifier, Asn.ObjectIdentifier.Pkcs7_Data))
				{
					while (Reader.BaseStream.Position < Length)
					{
						Asn.FieldInfo NextField = Asn.ReadField(Reader);
						if (NextField.Tag == Asn.FieldTag.OCTET_STRING)
						{
							byte[] Data = Reader.ReadBytes(NextField.Length);

							XmlDocument Document = new XmlDocument();
							Document.Load(new MemoryStream(Data));
							return Document;
						}
						else
						{
							Asn.SkipValue(Reader, NextField);
						}
					}
				}
			}
			else
			{
				Asn.SkipValue(Reader, Field);
			}
		}
		throw new Exception("No PKCS7-Data section");
	}
}

After the XML section has been extracted we can interact with this using the XML classes of C#, for example to request the value of an specific key, or to do a Dictionary with all the keys.

Dictionary<string, XmlElement> NameToValue = new Dictionary<string, XmlElement>();

foreach(XmlElement KeyElement in xmlDocument.SelectNodes("/plist/dict/key"))
{
	XmlNode ValueNode = KeyElement.NextSibling;
	while(ValueNode != null)
	{
		XmlElement ValueElement = ValueNode as XmlElement;
		if(ValueElement != null)
		{
			NameToValue[KeyElement.InnerText] = ValueElement;
			break;
		}
	}
}

And request the value using that Dictionary

XmlElement UniqueIdElement;
if(!NameToValue.TryGetValue("UUID", out UniqueIdElement))
{
	throw new BuildException("Missing UUID in MobileProvision");
}
return UniqueIdElement.InnerText;

We are going to display only the unique, team and the bundle identifier, so we are going to add one method to do the request of each of this values. This is the last modification of our MobileProvisionContents class:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;

namespace MobileProvisionReader
{
    class MobileProvisionContents
    {
		/// <summary>
		/// The contents of the provision
		/// </summary>
		XmlDocument Document;

		/// <summary>
		/// Map of key names to XML elements holding their values
		/// </summary>
		Dictionary<string, XmlElement> NameToValue = new Dictionary<string, XmlElement>();

		/// <summary>
		/// Constructor
		/// </summary>
		/// <param name="Document">XML file to create the mobile provision from. Call Read() to read from a signed file on disk.</param>
		public MobileProvisionContents(XmlDocument Document)
		{
			this.Document = Document;

			foreach (XmlElement KeyElement in Document.SelectNodes("/plist/dict/key"))
			{
				XmlNode ValueNode = KeyElement.NextSibling;
				while (ValueNode != null)
				{
					XmlElement ValueElement = ValueNode as XmlElement;
					if (ValueElement != null)
					{
						NameToValue[KeyElement.InnerText] = ValueElement;
						break;
					}
				}
			}
		}

		/// <summary>
		/// Gets the unique id for this mobileprovision
		/// </summary>
		/// <returns>UUID for the provision</returns>
		public string GetUniqueId()
		{
			XmlElement UniqueIdElement;
			if (!NameToValue.TryGetValue("UUID", out UniqueIdElement))
			{
				throw new Exception("Missing UUID in MobileProvision");
			}
			return UniqueIdElement.InnerText;
		}

		/// <summary>
		/// Gets the bundle id for this mobileprovision
		/// </summary>
		/// <returns>Bundle Identifier for the provision</returns>
		public string GetBundleIdentifier()
		{
			XmlElement UniqueIdElement = null, UniqueIdEntitlement;
			if (!NameToValue.TryGetValue("Entitlements", out UniqueIdEntitlement) || UniqueIdEntitlement.Name != "dict")
			{
				throw new Exception("Missing Entitlements in MobileProvision");
			}

			foreach (XmlElement KeyElement in UniqueIdEntitlement.SelectNodes("key"))
			{
				Console.WriteLine("Found entitlement node:" + KeyElement.InnerText);
				if (!KeyElement.InnerText.Equals("application-identifier"))
				{
					continue;
				}
				UniqueIdElement = KeyElement.NextSibling as XmlElement;
				break;
			}


			if (UniqueIdElement == null)
			{
				throw new Exception("Missing Bundle Identifier in MobileProvision");
			}
			return UniqueIdElement.InnerText.Substring(UniqueIdElement.InnerText.IndexOf('.') + 1);
		}

		/// <summary>
		/// Gets the team unique id for this mobileprovision
		/// </summary>
		/// <param name="UniqueId">Receives the team unique id</param>
		/// <returns>True if the team unique ID was found, false otherwise</returns>
		public bool TryGetTeamUniqueId(out string UniqueId)
		{
			XmlElement UniqueIdElement;
			if (!NameToValue.TryGetValue("TeamIdentifier", out UniqueIdElement) || UniqueIdElement.Name != "array")
			{
				UniqueId = null;
				return false;
			}

			XmlElement ValueElement = UniqueIdElement.SelectSingleNode("string") as XmlElement;
			if (ValueElement == null)
			{
				UniqueId = null;
				return false;
			}

			UniqueId = ValueElement.InnerText;
			return true;
		}

		/// <summary>
		/// Reads a mobileprovision from a file on disk
		/// </summary>
		/// <param name="Location">Path to the file</param>
		/// <returns>New mobile provision instance</returns>
		public static MobileProvisionContents Read(string Location)
		{
			XmlDocument Document = ReadXml(Location);
			return new MobileProvisionContents(Document);
		}

		/// <summary>
		/// Reads the plist file inside a mobileprovision
		/// </summary>
		/// <param name="Location">Path to the file</param>
		/// <returns>XML plist extracted from the mobile provision</returns>
		public static XmlDocument ReadXml(string Location)
		{
			// Provision data is stored as PKCS7-signed file in ASN.1 BER format
			using (BinaryReader Reader = new BinaryReader(File.Open(Location, FileMode.Open, FileAccess.Read)))
			{
				long Length = Reader.BaseStream.Length;
				while (Reader.BaseStream.Position < Length)
				{
					Asn.FieldInfo Field = Asn.ReadField(Reader);
					if (Field.Tag == Asn.FieldTag.OBJECT_IDENTIFIER)
					{
						int[] Identifier = Asn.ReadObjectIdentifier(Reader, Field.Length);
						if (Enumerable.SequenceEqual(Identifier, Asn.ObjectIdentifier.Pkcs7_Data))
						{
							while (Reader.BaseStream.Position < Length)
							{
								Asn.FieldInfo NextField = Asn.ReadField(Reader);
								if (NextField.Tag == Asn.FieldTag.OCTET_STRING)
								{
									byte[] Data = Reader.ReadBytes(NextField.Length);

									XmlDocument Document = new XmlDocument();
									Document.Load(new MemoryStream(Data));
									return Document;
								}
								else
								{
									Asn.SkipValue(Reader, NextField);
								}
							}
						}
					}
					else
					{
						Asn.SkipValue(Reader, Field);
					}
				}
				throw new Exception("No PKCS7-Data section");
			}
		}
	}
}

The program entry point must call to the Read method of this class to extract the xml data and our Get methods to request the value of our desired sections

static void Main(string[] args)
{
	try
	{
		//Read mobileprovision contents
		MobileProvisionContents mpc = MobileProvisionContents.Read("D://Selfsigned.mobileprovision");

		//Dispaly some mobileprovision entries
		string uuid = mpc.GetUniqueId();
		Console.WriteLine("UUID {0}", uuid);

		string teamUniqueId;
		if (mpc.TryGetTeamUniqueId(out teamUniqueId))
		{
			Console.WriteLine("TeamIdentifier: {0}", teamUniqueId);
		}
		else
		{
			Console.WriteLine("TeamIdentifier: missing");
		}

		string bundleid = mpc.GetBundleIdentifier();
		Console.WriteLine("BundleIdentifier: {0}", bundleid);

	}
	catch (Exception e)
	{
		Console.WriteLine("Reading ERROR Exception: {0}", e.Message);
	}
}

If this reader is able to display the selected values the mobileprovision is valid to compile iOS applications remotely using our UE4 Editor.

provisionreader_output

If this program throws an Exception our mobileprofile file has an invalid format, so we need to repeat the steps to regenerate again a new file. If we obtain a “Root element is missing” exception during a remote UE4 compilation process or while using our reader program the mobileprofile is corrupted.

Generating and uploading Crashlytics Data
ERROR: Unhandled exception: System.Xml.XmlException: Root element is missing.
	  at System.Xml.XmlTextReaderImpl.Throw (System.Exception e) [0x00027] in <c615384cc92144aaa3f942d0c781696a>:0
	  at System.Xml.XmlTextReaderImpl.ThrowWithoutLineInfo (System.String res) [0x00017] in <c615384cc92144aaa3f942d0c781696a>:0
	  at System.Xml.XmlTextReaderImpl.ParseDocumentContent () [0x0035d] in <c615384cc92144aaa3f942d0c781696a>:0
	  at System.Xml.XmlTextReaderImpl.Read () [0x0008c] in <c615384cc92144aaa3f942d0c781696a>:0
	  at System.Xml.XmlLoader.Load (System.Xml.XmlDocument doc, System.Xml.XmlReader reader, System.Boolean preserveWhitespace) [0x000a6] in <c615384cc92144aaa3f942d0c781696a>:0
	  at System.Xml.XmlDocument.Load (System.Xml.XmlReader reader) [0x0002e] in <c615384cc92144aaa3f942d0c781696a>:0
	  at System.Xml.XmlDocument.Load (System.IO.Stream inStream) [0x00013] in <c615384cc92144aaa3f942d0c781696a>:0
	  at UnrealBuildTool.MobileProvisionContents.ReadXml (Tools.DotNETCommon.FileReference Location) [0x00080] in <4308e3bdfb1844339db4a61e5a2da573>:0
	  at UnrealBuildTool.MobileProvisionContents.Read (Tools.DotNETCommon.FileReference Location) [0x00000] in <4308e3bdfb1844339db4a61e5a2da573>:0
	  at UnrealBuildTool.IOSToolChain.PostBuildSync (UnrealBuildTool.IOSPostBuildSyncTarget Target) [0x00586] in <4308e3bdfb1844339db4a61e5a2da573>:0
	  at UnrealBuildTool.IOSPostBuildSyncMode.Execute (Tools.DotNETCommon.CommandLineArguments Arguments) [0x00018] in <4308e3bdfb1844339db4a61e5a2da573>:0
	  at UnrealBuildTool.UnrealBuildTool.Main (System.String[] ArgumentsArray) [0x002bb] in <4308e3bdfb1844339db4a61e5a2da573>:0
PackagingResults: Error: Unhandled exception: System.Xml.XmlException: Root element is missing.

If we are sharing files between Windows and MacOS machines we must account for different line termination conventions. We need create our .plist file using the LF termination. Using CR/LF(Windows) termination will result in a invalid mobileprovision file.

line_termination_compare

Usually a good text editor has a tool to change the line termination, so we don’t need to change each end line manually.

text_editor_termination_tool

Other cause can be a syntax error in the XML, for example an unclosed tag, using a xml validator we can fix this quickly.

And that is all, if you find other problems leave a comment and we could find a solution together.

Downloads

Support this blog!

For the past year I've been dedicating more of my time to the creation of tutorials, mainly about game development. If you think these posts have either helped or inspired you, please consider supporting this blog. Thank you so much for your contribution!

Write a Comment

Comment