// dnlib: See LICENSE.txt for more info using System; using System.Collections.Generic; using System.Diagnostics; using System.Text; using dnlib.IO; using dnlib.PE; using dnlib.W32Resources; namespace dnlib.DotNet.Writer { /// /// Writes Win32 resources /// public sealed class Win32ResourcesChunk : IReuseChunk { readonly Win32Resources win32Resources; FileOffset offset; RVA rva; uint length; readonly Dictionary dirDict = new Dictionary(); readonly List dirList = new List(); readonly Dictionary dataHeaderDict = new Dictionary(); readonly List dataHeaderList = new List(); readonly Dictionary stringsDict = new Dictionary(StringComparer.Ordinal); readonly List stringsList = new List(); readonly Dictionary dataDict = new Dictionary(); readonly List dataList = new List(); /// public FileOffset FileOffset => offset; /// public RVA RVA => rva; /// /// Constructor /// /// Win32 resources public Win32ResourcesChunk(Win32Resources win32Resources) => this.win32Resources = win32Resources; /// /// Returns the and of a /// . must have been called. /// /// A /// Updated with the file offset /// Updated with the RVA /// true if is valid and /// and have been updated. false /// if is not part of the Win32 resources. public bool GetFileOffsetAndRvaOf(ResourceDirectoryEntry dirEntry, out FileOffset fileOffset, out RVA rva) { if (dirEntry is ResourceDirectory dir) return GetFileOffsetAndRvaOf(dir, out fileOffset, out rva); if (dirEntry is ResourceData dataHeader) return GetFileOffsetAndRvaOf(dataHeader, out fileOffset, out rva); fileOffset = 0; rva = 0; return false; } /// /// Returns the of a . /// must have been called. /// /// A /// The file offset or 0 if is invalid public FileOffset GetFileOffset(ResourceDirectoryEntry dirEntry) { GetFileOffsetAndRvaOf(dirEntry, out var fileOffset, out var rva); return fileOffset; } /// /// Returns the of a . /// must have been called. /// /// A /// The RVA or 0 if is invalid public RVA GetRVA(ResourceDirectoryEntry dirEntry) { GetFileOffsetAndRvaOf(dirEntry, out var fileOffset, out var rva); return rva; } /// /// Returns the and of a /// . must have been called. /// /// A /// Updated with the file offset /// Updated with the RVA /// true if is valid and /// and have been updated. false /// if is not part of the Win32 resources. public bool GetFileOffsetAndRvaOf(ResourceDirectory dir, out FileOffset fileOffset, out RVA rva) { if (dir is null || !dirDict.TryGetValue(dir, out uint offs)) { fileOffset = 0; rva = 0; return false; } fileOffset = offset + offs; rva = this.rva + offs; return true; } /// /// Returns the of a . /// must have been called. /// /// A /// The file offset or 0 if is invalid public FileOffset GetFileOffset(ResourceDirectory dir) { GetFileOffsetAndRvaOf(dir, out var fileOffset, out var rva); return fileOffset; } /// /// Returns the of a . /// must have been called. /// /// A /// The RVA or 0 if is invalid public RVA GetRVA(ResourceDirectory dir) { GetFileOffsetAndRvaOf(dir, out var fileOffset, out var rva); return rva; } /// /// Returns the and of a /// . must have been called. /// /// A /// Updated with the file offset /// Updated with the RVA /// true if is valid and /// and have been updated. false /// if is not part of the Win32 resources. public bool GetFileOffsetAndRvaOf(ResourceData dataHeader, out FileOffset fileOffset, out RVA rva) { if (dataHeader is null || !dataHeaderDict.TryGetValue(dataHeader, out uint offs)) { fileOffset = 0; rva = 0; return false; } fileOffset = offset + offs; rva = this.rva + offs; return true; } /// /// Returns the of a . /// must have been called. /// /// A /// The file offset or 0 if is invalid public FileOffset GetFileOffset(ResourceData dataHeader) { GetFileOffsetAndRvaOf(dataHeader, out var fileOffset, out var rva); return fileOffset; } /// /// Returns the of a . /// must have been called. /// /// A /// The RVA or 0 if is invalid public RVA GetRVA(ResourceData dataHeader) { GetFileOffsetAndRvaOf(dataHeader, out var fileOffset, out var rva); return rva; } /// /// Returns the and of a /// 's name. must have been /// called. /// /// The name of a /// Updated with the file offset /// Updated with the RVA /// true if is valid and /// and have been updated. false /// if is not part of the Win32 resources. public bool GetFileOffsetAndRvaOf(string name, out FileOffset fileOffset, out RVA rva) { if (name is null || !stringsDict.TryGetValue(name, out uint offs)) { fileOffset = 0; rva = 0; return false; } fileOffset = offset + offs; rva = this.rva + offs; return true; } /// /// Returns the of a 's name. /// must have been called. /// /// The name of a /// The file offset or 0 if is invalid public FileOffset GetFileOffset(string name) { GetFileOffsetAndRvaOf(name, out var fileOffset, out var rva); return fileOffset; } /// /// Returns the of a 's name. /// must have been called. /// /// The name of a /// The RVA or 0 if is invalid public RVA GetRVA(string name) { GetFileOffsetAndRvaOf(name, out var fileOffset, out var rva); return rva; } const uint RESOURCE_DIR_ALIGNMENT = 4; const uint RESOURCE_DATA_HEADER_ALIGNMENT = 4; const uint RESOURCE_STRING_ALIGNMENT = 2; const uint RESOURCE_DATA_ALIGNMENT = 4; bool IReuseChunk.CanReuse(RVA origRva, uint origSize) { Debug.Assert(rva != 0); if (rva == 0) throw new InvalidOperationException(); return length <= origSize; } internal bool CheckValidOffset(FileOffset offset) { GetMaxAlignment(offset, out var error); return error is null; } static uint GetMaxAlignment(FileOffset offset, out string error) { error = null; uint maxAlignment = 1; maxAlignment = Math.Max(maxAlignment, RESOURCE_DIR_ALIGNMENT); maxAlignment = Math.Max(maxAlignment, RESOURCE_DATA_HEADER_ALIGNMENT); maxAlignment = Math.Max(maxAlignment, RESOURCE_STRING_ALIGNMENT); maxAlignment = Math.Max(maxAlignment, RESOURCE_DATA_ALIGNMENT); if (((uint)offset & (maxAlignment - 1)) != 0) error = $"Win32 resources section isn't {maxAlignment}-byte aligned"; else if (maxAlignment > ModuleWriterBase.DEFAULT_WIN32_RESOURCES_ALIGNMENT) error = "maxAlignment > DEFAULT_WIN32_RESOURCES_ALIGNMENT"; return maxAlignment; } /// public void SetOffset(FileOffset offset, RVA rva) { // NOTE: This method can be called twice by NativeModuleWriter, see Metadata.SetOffset() for more info bool initAll = this.offset == 0; this.offset = offset; this.rva = rva; if (win32Resources is null) return; if (!initAll) { // If it's called a second time, re-do everything dirDict.Clear(); dirList.Clear(); dataHeaderDict.Clear(); dataHeaderList.Clear(); stringsDict.Clear(); stringsList.Clear(); dataDict.Clear(); dataList.Clear(); } FindDirectoryEntries(); // Place everything in the following order: // 1. All resource directories. The root is always first. // 2. All resource data headers. // 3. All the strings. // 4. All resource data. uint rsrcOffset = 0; var maxAlignment = GetMaxAlignment(offset, out var error); if (error is not null) throw new ModuleWriterException(error); foreach (var dir in dirList) { rsrcOffset = Utils.AlignUp(rsrcOffset, RESOURCE_DIR_ALIGNMENT); dirDict[dir] = rsrcOffset; if (dir != dirList[0]) AddString(dir.Name); rsrcOffset += 16 + (uint)(dir.Directories.Count + dir.Data.Count) * 8; } foreach (var data in dataHeaderList) { rsrcOffset = Utils.AlignUp(rsrcOffset, RESOURCE_DATA_HEADER_ALIGNMENT); dataHeaderDict[data] = rsrcOffset; AddString(data.Name); AddData(data); rsrcOffset += 16; } foreach (var s in stringsList) { rsrcOffset = Utils.AlignUp(rsrcOffset, RESOURCE_STRING_ALIGNMENT); stringsDict[s] = rsrcOffset; rsrcOffset += 2 + (uint)(s.Length * 2); } foreach (var data in dataList) { rsrcOffset = Utils.AlignUp(rsrcOffset, RESOURCE_DATA_ALIGNMENT); dataDict[data] = rsrcOffset; rsrcOffset += data.CreateReader().Length; } length = rsrcOffset; } void AddData(ResourceData data) { if (dataDict.ContainsKey(data)) return; dataList.Add(data); dataDict.Add(data, 0); } void AddString(ResourceName name) { if (!name.HasName || stringsDict.ContainsKey(name.Name)) return; stringsList.Add(name.Name); stringsDict.Add(name.Name, 0); } void FindDirectoryEntries() => FindDirectoryEntries(win32Resources.Root); void FindDirectoryEntries(ResourceDirectory dir) { if (dirDict.ContainsKey(dir)) return; dirList.Add(dir); dirDict[dir] = 0; var dirs = dir.Directories; int count = dirs.Count; for (int i = 0; i < count; i++) FindDirectoryEntries(dirs[i]); var dirData = dir.Data; count = dirData.Count; for (int i = 0; i < count; i++) { var data = dirData[i]; if (dataHeaderDict.ContainsKey(data)) continue; dataHeaderList.Add(data); dataHeaderDict[data] = 0; } } /// public uint GetFileLength() => length; /// public uint GetVirtualSize() => GetFileLength(); /// public uint CalculateAlignment() => 0; /// public void WriteTo(DataWriter writer) { uint offset = 0; // The order here must be the same as in SetOffset() foreach (var dir in dirList) { uint padding = Utils.AlignUp(offset, RESOURCE_DIR_ALIGNMENT) - offset; writer.WriteZeroes((int)padding); offset += padding; if (dirDict[dir] != offset) throw new ModuleWriterException("Invalid Win32 resource directory offset"); offset += WriteTo(writer, dir); } foreach (var dataHeader in dataHeaderList) { uint padding = Utils.AlignUp(offset, RESOURCE_DATA_HEADER_ALIGNMENT) - offset; writer.WriteZeroes((int)padding); offset += padding; if (dataHeaderDict[dataHeader] != offset) throw new ModuleWriterException("Invalid Win32 resource data header offset"); offset += WriteTo(writer, dataHeader); } foreach (var s in stringsList) { uint padding = Utils.AlignUp(offset, RESOURCE_STRING_ALIGNMENT) - offset; writer.WriteZeroes((int)padding); offset += padding; if (stringsDict[s] != offset) throw new ModuleWriterException("Invalid Win32 resource string offset"); var bytes = Encoding.Unicode.GetBytes(s); if (bytes.Length / 2 > ushort.MaxValue) throw new ModuleWriterException("Win32 resource entry name is too long"); writer.WriteUInt16((ushort)(bytes.Length / 2)); writer.WriteBytes(bytes); offset += 2 + (uint)bytes.Length; } var dataBuffer = new byte[0x2000]; foreach (var data in dataList) { uint padding = Utils.AlignUp(offset, RESOURCE_DATA_ALIGNMENT) - offset; writer.WriteZeroes((int)padding); offset += padding; if (dataDict[data] != offset) throw new ModuleWriterException("Invalid Win32 resource data offset"); var reader = data.CreateReader(); offset += reader.BytesLeft; reader.CopyTo(writer, dataBuffer); } } uint WriteTo(DataWriter writer, ResourceDirectory dir) { writer.WriteUInt32(dir.Characteristics); writer.WriteUInt32(dir.TimeDateStamp); writer.WriteUInt16(dir.MajorVersion); writer.WriteUInt16(dir.MinorVersion); GetNamedAndIds(dir, out var named, out var ids); if (named.Count > ushort.MaxValue || ids.Count > ushort.MaxValue) throw new ModuleWriterException("Too many named/id Win32 resource entries"); writer.WriteUInt16((ushort)named.Count); writer.WriteUInt16((ushort)ids.Count); // These must be sorted in ascending order. Names are case insensitive. named.Sort((a, b) => a.Name.Name.ToUpperInvariant().CompareTo(b.Name.Name.ToUpperInvariant())); ids.Sort((a, b) => a.Name.Id.CompareTo(b.Name.Id)); foreach (var d in named) { writer.WriteUInt32(0x80000000 | stringsDict[d.Name.Name]); writer.WriteUInt32(GetDirectoryEntryOffset(d)); } foreach (var d in ids) { writer.WriteInt32(d.Name.Id); writer.WriteUInt32(GetDirectoryEntryOffset(d)); } return 16 + (uint)(named.Count + ids.Count) * 8; } uint GetDirectoryEntryOffset(ResourceDirectoryEntry e) { if (e is ResourceData) return dataHeaderDict[(ResourceData)e]; return 0x80000000 | dirDict[(ResourceDirectory)e]; } static void GetNamedAndIds(ResourceDirectory dir, out List named, out List ids) { named = new List(); ids = new List(); var dirs = dir.Directories; int count = dirs.Count; for (int i = 0; i < count; i++) { var d = dirs[i]; if (d.Name.HasId) ids.Add(d); else named.Add(d); } var dirData = dir.Data; count = dirData.Count; for (int i = 0; i < count; i++) { var d = dirData[i]; if (d.Name.HasId) ids.Add(d); else named.Add(d); } } uint WriteTo(DataWriter writer, ResourceData dataHeader) { writer.WriteUInt32((uint)rva + dataDict[dataHeader]); writer.WriteUInt32((uint)dataHeader.CreateReader().Length); writer.WriteUInt32(dataHeader.CodePage); writer.WriteUInt32(dataHeader.Reserved); return 16; } } }