// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; using System.Xml.Serialization; namespace WPinternals { [XmlType("Partitions")] public class GPT { private byte[] GPTBuffer; private UInt32 HeaderOffset; private UInt32 HeaderSize; private UInt32 TableOffset; private UInt32 TableSize; private UInt32 PartitionEntrySize; private UInt32 MaxPartitions; internal UInt64 FirstUsableSector; internal UInt64 LastUsableSector; internal bool HasChanged = false; [XmlElement("Partition")] public List Partitions = new List(); public GPT() // Only for serialization { } internal GPT(byte[] GPTBuffer) { this.GPTBuffer = GPTBuffer; UInt32? TempHeaderOffset = ByteOperations.FindAscii(GPTBuffer, "EFI PART"); if (TempHeaderOffset == null) throw new WPinternalsException("Bad GPT"); HeaderOffset = (UInt32)TempHeaderOffset; HeaderSize = ByteOperations.ReadUInt32(GPTBuffer, HeaderOffset + 0x0C); TableOffset = HeaderOffset + 0x200; FirstUsableSector = ByteOperations.ReadUInt64(GPTBuffer, HeaderOffset + 0x28); LastUsableSector = ByteOperations.ReadUInt64(GPTBuffer, HeaderOffset + 0x30); MaxPartitions = ByteOperations.ReadUInt32(GPTBuffer, HeaderOffset + 0x50); PartitionEntrySize = ByteOperations.ReadUInt32(GPTBuffer, HeaderOffset + 0x54); TableSize = MaxPartitions * PartitionEntrySize; if ((TableOffset + TableSize) > GPTBuffer.Length) throw new WPinternalsException("Bad GPT"); UInt32 PartitionOffset = TableOffset; while (PartitionOffset < (TableOffset + TableSize)) { string Name = ByteOperations.ReadUnicodeString(GPTBuffer, PartitionOffset + 0x38, 0x48).TrimEnd(new char[] { (char)0, ' ' }); if (Name.Length == 0) break; Partition CurrentPartition = new Partition(); CurrentPartition.Name = Name; CurrentPartition.FirstSector = ByteOperations.ReadUInt64(GPTBuffer, PartitionOffset + 0x20); CurrentPartition.LastSector = ByteOperations.ReadUInt64(GPTBuffer, PartitionOffset + 0x28); CurrentPartition.PartitionTypeGuid = ByteOperations.ReadGuid(GPTBuffer, PartitionOffset + 0x00); CurrentPartition.PartitionGuid = ByteOperations.ReadGuid(GPTBuffer, PartitionOffset + 0x10); CurrentPartition.Attributes = ByteOperations.ReadUInt64(GPTBuffer, PartitionOffset + 0x30); Partitions.Add(CurrentPartition); PartitionOffset += PartitionEntrySize; } HasChanged = false; } internal Partition GetPartition(string Name) { return Partitions.Where(p => (string.Compare(p.Name, Name, true) == 0)).FirstOrDefault(); } // Magic! // SecureBoot hack for Bootloader Spec A starts here internal byte[] InsertHack() { Partition HackPartition = Partitions.Where(p => (p.Name == "HACK")).FirstOrDefault(); Partition SBL1 = Partitions.Where(p => (p.Name == "SBL1")).FirstOrDefault(); Partition SBL2 = Partitions.Where(p => (p.Name == "SBL2")).FirstOrDefault(); if ((SBL1 == null) || (SBL2 == null)) throw new WPinternalsException("Bad GPT"); if (HackPartition == null) { HackPartition = new Partition(); HackPartition.Name = "HACK"; HackPartition.Attributes = SBL2.Attributes; HackPartition.FirstSector = SBL1.LastSector; HackPartition.LastSector = SBL1.LastSector; HackPartition.PartitionTypeGuid = SBL2.PartitionTypeGuid; HackPartition.PartitionGuid = SBL2.PartitionGuid; Partitions.Add(HackPartition); SBL1.LastSector--; SBL2.PartitionTypeGuid = new Guid(new byte[] { 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74 }); SBL2.PartitionGuid = new Guid(new byte[] { 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74 }); } HasChanged = true; return Rebuild(); } internal byte[] RemoveHack() { Partition HackPartition = Partitions.Where(p => (p.Name == "HACK")).FirstOrDefault(); Partition SBL1 = Partitions.Where(p => (p.Name == "SBL1")).FirstOrDefault(); Partition SBL2 = Partitions.Where(p => (p.Name == "SBL2")).FirstOrDefault(); if ((SBL1 == null) || (SBL2 == null)) throw new WPinternalsException("Bad GPT"); if (HackPartition != null) { SBL2.PartitionTypeGuid = HackPartition.PartitionTypeGuid; SBL2.PartitionGuid = HackPartition.PartitionGuid; Partitions.Remove(HackPartition); SBL1.LastSector++; } HasChanged = true; return Rebuild(); } internal byte[] Rebuild() { if (GPTBuffer == null) { TableSize = 0x4200; TableOffset = 0; GPTBuffer = new byte[TableSize]; } else Array.Clear(GPTBuffer, (int)TableOffset, (int)TableSize); UInt32 PartitionOffset = TableOffset; foreach (Partition CurrentPartition in Partitions) { ByteOperations.WriteGuid(GPTBuffer, PartitionOffset + 0x00, CurrentPartition.PartitionTypeGuid); ByteOperations.WriteGuid(GPTBuffer, PartitionOffset + 0x10, CurrentPartition.PartitionGuid); ByteOperations.WriteUInt64(GPTBuffer, PartitionOffset + 0x20, CurrentPartition.FirstSector); ByteOperations.WriteUInt64(GPTBuffer, PartitionOffset + 0x28, CurrentPartition.LastSector); ByteOperations.WriteUInt64(GPTBuffer, PartitionOffset + 0x30, CurrentPartition.Attributes); ByteOperations.WriteUnicodeString(GPTBuffer, PartitionOffset + 0x38, CurrentPartition.Name, 0x48); PartitionOffset += PartitionEntrySize; } ByteOperations.WriteUInt32(GPTBuffer, HeaderOffset + 0x58, ByteOperations.CRC32(GPTBuffer, TableOffset, TableSize)); ByteOperations.WriteUInt32(GPTBuffer, HeaderOffset + 0x10, 0); ByteOperations.WriteUInt32(GPTBuffer, HeaderOffset + 0x10, ByteOperations.CRC32(GPTBuffer, HeaderOffset, HeaderSize)); return GPTBuffer; } internal void MergePartitionsFromFile(string Path, bool RoundToChunks) { MergePartitions(File.ReadAllText(Path), RoundToChunks); } internal void MergePartitionsFromStream(Stream Partitions, bool RoundToChunks) { using (TextReader tr = new StreamReader(Partitions)) { MergePartitions(tr.ReadToEnd(), RoundToChunks); } } internal void MergePartitions(string Xml, bool RoundToChunks, ZipArchive Archive = null) { GPT GptToMerge; if (Xml == null) GptToMerge = new GPT(); else { XmlSerializer x = new XmlSerializer(typeof(GPT), ""); MemoryStream s = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(Xml)); GptToMerge = (GPT)x.Deserialize(s); s.Dispose(); } if (Archive != null) { foreach (Partition NewPartition in GptToMerge.Partitions) { ZipArchiveEntry Entry = Archive.Entries.Where(e => ((string.Compare(e.Name, NewPartition.Name, true) == 0) || (e.Name.StartsWith(NewPartition.Name + ".", true, System.Globalization.CultureInfo.GetCultureInfo("en-US"))))).FirstOrDefault(); if (Entry == null) { // There is a partition entry in the xml, but this partition is not present in the archive. Partition OldPartition = GetPartition(NewPartition.Name); if (OldPartition == null) { // The partition entry in the xml is also not present in the current partition table. // It must have a know position and length. if (NewPartition.LastSector == 0) { throw new WPinternalsException("Unknown length for partition \"" + NewPartition.Name + "\""); } } else { // The partition entry in the xml is also present in the current partition table. // But since the partition is not present in the archive, the partition cannot be relocated. // If the location of the new partition is specified, it must be the same as the current partition. if ((NewPartition.FirstSector != 0) && (NewPartition.FirstSector != OldPartition.FirstSector)) throw new WPinternalsException("Incorrect location for partition \"" + NewPartition.Name + "\""); if ((NewPartition.LastSector != 0) && (NewPartition.LastSector != OldPartition.LastSector)) throw new WPinternalsException("Incorrect length for partition \"" + NewPartition.Name + "\""); NewPartition.FirstSector = OldPartition.FirstSector; NewPartition.LastSector = OldPartition.LastSector; } } else { // The partition in the xml is also present in the archive. // If the length is specified in the xml, it must match the file in the archive. ulong StreamLengthInSectors = (ulong)Entry.Length / 0x200; using (DecompressedStream DecompressedStream = new DecompressedStream(Entry.Open())) { try { StreamLengthInSectors = (ulong)DecompressedStream.Length / 0x200; } catch { } } if (NewPartition.LastSector == 0) { NewPartition.SizeInSectors = StreamLengthInSectors; } else { if (NewPartition.SizeInSectors != StreamLengthInSectors) throw new WPinternalsException("Inconsistent length specified for partition \"" + NewPartition.Name + "\""); } } } } else { foreach (Partition NewPartition in GptToMerge.Partitions) { // This is a partition entry in the xml, and there is no archive. Partition OldPartition = GetPartition(NewPartition.Name); if (OldPartition == null) { // The partition entry in the xml is also not present in the current partition table. // It must have a known position and length. if (NewPartition.LastSector == 0) { throw new WPinternalsException("Unknown length for partition \"" + NewPartition.Name + "\""); } } else { // The partition entry in the xml is also present in the current partition table. // But since the partition is not present in the archive, the partition cannot be relocated. // If the location of the new partition is specified, it must be the same as the current partition. if ((NewPartition.FirstSector != 0) && (NewPartition.FirstSector != OldPartition.FirstSector)) throw new WPinternalsException("Incorrect location for partition \"" + NewPartition.Name + "\""); if ((NewPartition.LastSector != 0) && (NewPartition.LastSector != OldPartition.LastSector)) throw new WPinternalsException("Incorrect length for partition \"" + NewPartition.Name + "\""); NewPartition.FirstSector = OldPartition.FirstSector; NewPartition.LastSector = OldPartition.LastSector; } } } List DynamicPartitions = new List(); if (Archive != null) { // Partitions which are present in the archive, and which have no start-sector in the new GPT data (dynamic relocation), // and which can be clustered to the end of emmc, are first removed from the existing GPT. IEnumerable SortedPartitions = Partitions.OrderBy(p => p.FirstSector); for (int i = SortedPartitions.Count() - 1; i >= 0; i--) { Partition OldPartition = SortedPartitions.ElementAt(i); // Present in archive? ZipArchiveEntry Entry = Archive.Entries.Where(e => ((string.Compare(e.Name, OldPartition.Name, true) == 0) || (e.Name.StartsWith(OldPartition.Name + ".", true, System.Globalization.CultureInfo.GetCultureInfo("en-US"))))).FirstOrDefault(); if (Entry != null) { // Not present in new GPT or present in GPT without FirstSector? Partition NewPartition = GptToMerge.GetPartition(OldPartition.Name); if ((NewPartition == null) || (NewPartition.FirstSector == 0)) { DynamicPartitions.Insert(0, OldPartition); this.Partitions.Remove(OldPartition); } else break; } else break; } } // All partitions in the new GPT data should have a start-sector and end-sector by now. // The partitions in the new GPT data will be applied to the current partition-table. // Existing partitions, which are overwritten by the new partitions will be removed from the existing GPT. // Existing partition with the same name in the existing GPT is reused (guids and attribs remain, if not specified). UInt64 LowestSector = 0; Partition DPP = this.GetPartition("DPP"); if (DPP != null) LowestSector = DPP.LastSector + 1; foreach (Partition NewPartition in GptToMerge.Partitions) { // If the new partition is a dynamic partition, then skip it here. It will be added later. if (DynamicPartitions.Select(p => p.Name).Any(n => string.Compare(n, NewPartition.Name, true) == 0)) continue; // Sanity check if (NewPartition.FirstSector < LowestSector) throw new WPinternalsException("Bad sector alignment for partition: " + NewPartition.Name); Partition CurrentPartition = this.GetPartition(NewPartition.Name); if (CurrentPartition == null) { CurrentPartition = new Partition(); CurrentPartition.Name = NewPartition.Name; this.Partitions.Add(CurrentPartition); HasChanged = true; } if ((NewPartition.FirstSector != 0) && (NewPartition.FirstSector != CurrentPartition.FirstSector)) { CurrentPartition.FirstSector = NewPartition.FirstSector; HasChanged = true; } if ((NewPartition.LastSector != 0) && (NewPartition.LastSector != CurrentPartition.LastSector)) { CurrentPartition.LastSector = NewPartition.LastSector; HasChanged = true; } if ((NewPartition.Attributes != 0) && (CurrentPartition.Attributes != NewPartition.Attributes)) { CurrentPartition.Attributes = NewPartition.Attributes; HasChanged = true; } if ((NewPartition.PartitionGuid == null) || (NewPartition.PartitionGuid != CurrentPartition.PartitionGuid)) HasChanged = true; if (NewPartition.PartitionGuid != null) CurrentPartition.PartitionGuid = NewPartition.PartitionGuid; if (CurrentPartition.PartitionGuid == null) CurrentPartition.PartitionGuid = Guid.NewGuid(); if ((NewPartition.PartitionTypeGuid == null) || (NewPartition.PartitionTypeGuid != CurrentPartition.PartitionTypeGuid)) HasChanged = true; if (NewPartition.PartitionTypeGuid != null) CurrentPartition.PartitionTypeGuid = NewPartition.PartitionTypeGuid; if (CurrentPartition.PartitionTypeGuid == null) CurrentPartition.PartitionTypeGuid = Guid.NewGuid(); for (int i = this.Partitions.Count - 1; i >= 0; i--) { if (this.Partitions[i] != CurrentPartition) { if ((CurrentPartition.FirstSector <= this.Partitions[i].LastSector) && (CurrentPartition.LastSector >= this.Partitions[i].FirstSector)) { this.Partitions.RemoveAt(i); HasChanged = true; } } } } if (Archive != null) { // All partitions listed in the archive, which are present in the existing GPT, should overwrite the existing partition. // Check if the sizes of the partitions in the archive do not exceed the size of the partition, as listed in the GPT. foreach (Partition OldPartition in this.Partitions) { ZipArchiveEntry Entry = Archive.Entries.Where(e => ((string.Compare(e.Name, OldPartition.Name, true) == 0) || (e.Name.StartsWith(OldPartition.Name + ".", true, System.Globalization.CultureInfo.GetCultureInfo("en-US"))))).FirstOrDefault(); if (Entry != null) { DecompressedStream DecompressedStream = new DecompressedStream(Entry.Open()); ulong StreamLengthInSectors = (ulong)Entry.Length / 0x200; try { StreamLengthInSectors = (ulong)DecompressedStream.Length / 0x200; } catch { } DecompressedStream.Close(); UInt64 MaxPartitionSizeInSectors = OldPartition.SizeInSectors; Partition NextPartition = this.Partitions.Where(p => p.FirstSector > OldPartition.FirstSector).OrderBy(p => p.FirstSector).FirstOrDefault(); if (NextPartition != null) MaxPartitionSizeInSectors = NextPartition.FirstSector - OldPartition.FirstSector; if (StreamLengthInSectors > MaxPartitionSizeInSectors) { throw new WPinternalsException("Incorrect length for partition \"" + OldPartition.Name + "\""); } if (OldPartition.SizeInSectors != StreamLengthInSectors) { OldPartition.SizeInSectors = StreamLengthInSectors; HasChanged = true; } } } // All remaining partitions in the archive, which were listed in the original GPT, // should be added at the end of the partition-table. UInt64 FirstFreeSector = 0x5000; if (this.Partitions.Count > 0) { FirstFreeSector = this.Partitions.Max(p => p.LastSector) + 1; // Always start a new partition on a new chunk (0x100 sector boundary), to be more flexible during custom flash if (RoundToChunks && (((double)FirstFreeSector % 0x100) != 0)) FirstFreeSector = (UInt64)(Math.Ceiling((double)FirstFreeSector / 0x100) * 0x100); } foreach (Partition NewPartition in DynamicPartitions) { if (NewPartition.FirstSector != FirstFreeSector) { NewPartition.FirstSector = FirstFreeSector; HasChanged = true; } ZipArchiveEntry Entry = Archive.Entries.Where(e => ((string.Compare(e.Name, NewPartition.Name, true) == 0) || (e.Name.StartsWith(NewPartition.Name + ".", true, System.Globalization.CultureInfo.GetCultureInfo("en-US"))))).FirstOrDefault(); DecompressedStream DecompressedStream = new DecompressedStream(Entry.Open()); ulong StreamLengthInSectors = (ulong)Entry.Length / 0x200; try { StreamLengthInSectors = (ulong)DecompressedStream.Length / 0x200; } catch { } DecompressedStream.Close(); if (NewPartition.SizeInSectors != StreamLengthInSectors) { NewPartition.SizeInSectors = StreamLengthInSectors; HasChanged = true; } this.Partitions.Add(NewPartition); FirstFreeSector += StreamLengthInSectors; // Always start a new partition on a new chunk (0x100 sector boundary), to be more flexible during custom flash if (RoundToChunks && (((double)FirstFreeSector % 0x100) != 0)) FirstFreeSector = (UInt64)(Math.Ceiling((double)FirstFreeSector / 0x100) * 0x100); } } Rebuild(); } internal void WritePartitions(string Path) { string DirPath = System.IO.Path.GetDirectoryName(Path); if (!Directory.Exists(DirPath)) Directory.CreateDirectory(DirPath); XmlSerializer x = new XmlSerializer(typeof(GPT), ""); XmlSerializerNamespaces ns = new XmlSerializerNamespaces(); ns.Add("", ""); System.IO.StreamWriter FileWriter = new System.IO.StreamWriter(Path); x.Serialize(FileWriter, this, ns); FileWriter.Close(); } internal static GPT ReadPartitions(string Path) { XmlSerializer x = new XmlSerializer(typeof(GPT), ""); using (FileStream s = new FileStream(Path, FileMode.Open)) { return (GPT)x.Deserialize(s); } } internal void RestoreBackupPartitions() { // This is necessary, because the partitions and backup-partitions can exchange. // This may cause the startsector to be higher than the maximum allowed sector for flashing with a Lumia V1 programmer (hardcoded in programmer) List RevisePartitions = new List(new string[] { "SBL1", "SBL2", "SBL3", "UEFI", "TZ", "RPM", "WINSECAPP" }); foreach (string RevisePartitionName in RevisePartitions) { Partition RevisePartition = GetPartition(RevisePartitionName); Partition ReviseBackupPartition = GetPartition("BACKUP_" + RevisePartitionName); if ((RevisePartition != null) && (ReviseBackupPartition != null) && (RevisePartition.FirstSector > ReviseBackupPartition.FirstSector)) { ulong OriginalFirstSector = RevisePartition.FirstSector; ulong OriginalLastSector = RevisePartition.LastSector; RevisePartition.FirstSector = ReviseBackupPartition.FirstSector; RevisePartition.LastSector = ReviseBackupPartition.LastSector; ReviseBackupPartition.FirstSector = OriginalFirstSector; ReviseBackupPartition.LastSector = OriginalLastSector; HasChanged = true; } if (RevisePartition.LastSector >= 0xF400) throw new WPinternalsException("Unsupported partition layout!"); } } } public class Partition { private UInt64 _SizeInSectors; private UInt64 _FirstSector; private UInt64 _LastSector; public string Name; // 0x48 public Guid PartitionTypeGuid; // 0x10 public Guid PartitionGuid; // 0x10 [XmlIgnore] internal UInt64 Attributes; // 0x08 [XmlIgnore] internal UInt64 SizeInSectors { get { if (_SizeInSectors != 0) return _SizeInSectors; else return LastSector - FirstSector + 1; } set { _SizeInSectors = value; if (FirstSector != 0) LastSector = FirstSector + _SizeInSectors - 1; } } [XmlIgnore] internal UInt64 FirstSector // 0x08 { get { return _FirstSector; } set { _FirstSector = value; if (_SizeInSectors != 0) _LastSector = FirstSector + _SizeInSectors - 1; } } [XmlIgnore] internal UInt64 LastSector // 0x08 { get { return _LastSector; } set { _LastSector = value; _SizeInSectors = 0; } } [XmlIgnore] public string Volume { get { return @"\\?\Volume" + PartitionGuid.ToString("b") + @"\"; } } [XmlElement(ElementName = "FirstSector")] public string FirstSectorAsString { get { return "0x" + FirstSector.ToString("X16"); } set { FirstSector = Convert.ToUInt64(value, 16); } } [XmlElement(ElementName = "LastSector")] public string LastSectorAsString { get { return "0x" + LastSector.ToString("X16"); } set { LastSector = Convert.ToUInt64(value, 16); } } [XmlElement(ElementName = "Attributes")] public string AttributesAsString { get { return "0x" + Attributes.ToString("X16"); } set { Attributes = Convert.ToUInt64(value, 16); } } } }