From 9d437294e4d4a900ebed671fa21238b2effb2ac5 Mon Sep 17 00:00:00 2001 From: David Walter Date: Fri, 1 Dec 2023 18:03:26 +0100 Subject: [PATCH 1/3] Refactor PEFile --- .../WhiskyKit/{PE => }/BitmapInfo.swift | 12 +- .../Extensions/FileHandle+Extensions.swift | 13 + .../Sources/WhiskyKit/PE/COFFFileHeader.swift | 60 +++ WhiskyKit/Sources/WhiskyKit/PE/Magic.swift | 40 ++ .../Sources/WhiskyKit/PE/OptionalHeader.swift | 144 +++++++ .../WhiskyKit/PE/PortableExecutable.swift | 383 ++++++------------ .../WhiskyKit/PE/RSRC/ResourceDataEntry.swift | 51 +++ .../PE/RSRC/ResourceDirectoryEntry.swift | 52 +++ .../PE/RSRC/ResourceDirectoryTable.swift | 125 ++++++ .../RSRC/ResourceType.swift} | 26 +- .../WhiskyKit/PE/ResourceSection.swift | 213 ---------- WhiskyKit/Sources/WhiskyKit/PE/Section.swift | 72 ++++ .../WhiskyKit/{PE => }/ShellLink.swift | 18 +- 13 files changed, 720 insertions(+), 489 deletions(-) rename WhiskyKit/Sources/WhiskyKit/{PE => }/BitmapInfo.swift (96%) create mode 100644 WhiskyKit/Sources/WhiskyKit/PE/COFFFileHeader.swift create mode 100644 WhiskyKit/Sources/WhiskyKit/PE/Magic.swift create mode 100644 WhiskyKit/Sources/WhiskyKit/PE/OptionalHeader.swift create mode 100644 WhiskyKit/Sources/WhiskyKit/PE/RSRC/ResourceDataEntry.swift create mode 100644 WhiskyKit/Sources/WhiskyKit/PE/RSRC/ResourceDirectoryEntry.swift create mode 100644 WhiskyKit/Sources/WhiskyKit/PE/RSRC/ResourceDirectoryTable.swift rename WhiskyKit/Sources/WhiskyKit/{Extensions/Data+Extensions.swift => PE/RSRC/ResourceType.swift} (61%) delete mode 100644 WhiskyKit/Sources/WhiskyKit/PE/ResourceSection.swift create mode 100644 WhiskyKit/Sources/WhiskyKit/PE/Section.swift rename WhiskyKit/Sources/WhiskyKit/{PE => }/ShellLink.swift (89%) diff --git a/WhiskyKit/Sources/WhiskyKit/PE/BitmapInfo.swift b/WhiskyKit/Sources/WhiskyKit/BitmapInfo.swift similarity index 96% rename from WhiskyKit/Sources/WhiskyKit/PE/BitmapInfo.swift rename to WhiskyKit/Sources/WhiskyKit/BitmapInfo.swift index e19626bd..a189cb7a 100644 --- a/WhiskyKit/Sources/WhiskyKit/PE/BitmapInfo.swift +++ b/WhiskyKit/Sources/WhiskyKit/BitmapInfo.swift @@ -35,7 +35,7 @@ public struct BitmapInfoHeader: Hashable { public let originDirection: BitmapOriginDirection public let colorFormat: ColorFormat - init(handle: FileHandle, offset: Int) { + init(handle: FileHandle, offset: UInt64) { var offset = offset self.size = handle.extract(UInt32.self, offset: offset) ?? 0 offset += 4 @@ -65,7 +65,7 @@ public struct BitmapInfoHeader: Hashable { } // swiftlint:disable:next cyclomatic_complexity function_body_length - func renderBitmap(handle: FileHandle, offset: Int) -> NSImage { + func renderBitmap(handle: FileHandle, offset: UInt64) -> NSImage? { var offset = offset let colorTable = buildColorTable(offset: &offset, handle: handle) @@ -147,7 +147,7 @@ public struct BitmapInfoHeader: Hashable { return constructImage(pixels: pixels) } - func buildColorTable(offset: inout Int, handle: FileHandle) -> [ColorQuad] { + func buildColorTable(offset: inout UInt64, handle: FileHandle) -> [ColorQuad] { var colorTable: [ColorQuad] = [] for _ in 0.. NSImage { + func constructImage(pixels: [ColorQuad]) -> NSImage? { var pixels = pixels - if pixels.count > 0 { + if !pixels.isEmpty { let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.last.rawValue) let quadStride = MemoryLayout.stride @@ -192,7 +192,7 @@ public struct BitmapInfoHeader: Hashable { } } - return NSImage() + return nil } } diff --git a/WhiskyKit/Sources/WhiskyKit/Extensions/FileHandle+Extensions.swift b/WhiskyKit/Sources/WhiskyKit/Extensions/FileHandle+Extensions.swift index a975dd10..319053b5 100644 --- a/WhiskyKit/Sources/WhiskyKit/Extensions/FileHandle+Extensions.swift +++ b/WhiskyKit/Sources/WhiskyKit/Extensions/FileHandle+Extensions.swift @@ -20,6 +20,19 @@ import Foundation import os.log extension FileHandle { + func extract(_ type: T.Type, offset: UInt64 = 0) -> T? { + do { + try self.seek(toOffset: offset) + if let data = try self.read(upToCount: MemoryLayout.size) { + return data.withUnsafeBytes { $0.loadUnaligned(as: T.self)} + } else { + return nil + } + } catch { + return nil + } + } + func write(line: String) { do { guard let data = line.data(using: .utf8) else { return } diff --git a/WhiskyKit/Sources/WhiskyKit/PE/COFFFileHeader.swift b/WhiskyKit/Sources/WhiskyKit/PE/COFFFileHeader.swift new file mode 100644 index 00000000..94ac76f6 --- /dev/null +++ b/WhiskyKit/Sources/WhiskyKit/PE/COFFFileHeader.swift @@ -0,0 +1,60 @@ +// +// PortableExecutable+COFFFileHeader.swift +// WhiskyKit +// +// This file is part of Whisky. +// +// Whisky 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. +// +// Whisky 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 Whisky. +// If not, see https://www.gnu.org/licenses/. +// + +import Foundation + +extension PEFile { + /// COFF File Header (Object and Image) + /// + /// https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#coff-file-header-object-and-image + public struct COFFFileHeader: Hashable, Equatable { + public let machine: UInt16 + public let numberOfSections: UInt16 + public let timeDateStamp: Date + public let pointerToSymbolTable: UInt32 + public let numberOfSymbols: UInt32 + public let sizeOfOptionalHeader: UInt16 + public let characteristics: UInt16 + + init(handle: FileHandle, offset: UInt64) { + var offset = offset + 4 // Skip signature + + self.machine = handle.extract(UInt16.self, offset: offset) ?? 0 + offset += 2 + + self.numberOfSections = handle.extract(UInt16.self, offset: offset) ?? 0 + offset += 2 + + let timeDateStamp = handle.extract(UInt32.self, offset: offset) ?? 0 + self.timeDateStamp = Date(timeIntervalSince1970: TimeInterval(timeDateStamp)) + offset += 4 + + self.pointerToSymbolTable = handle.extract(UInt32.self, offset: offset) ?? 0 + offset += 4 + + self.numberOfSymbols = handle.extract(UInt32.self, offset: offset) ?? 0 + offset += 4 + + self.sizeOfOptionalHeader = handle.extract(UInt16.self, offset: offset) ?? 0 + offset += 2 + + self.characteristics = handle.extract(UInt16.self, offset: offset) ?? 0 + offset += 2 + } + } +} diff --git a/WhiskyKit/Sources/WhiskyKit/PE/Magic.swift b/WhiskyKit/Sources/WhiskyKit/PE/Magic.swift new file mode 100644 index 00000000..391eea0e --- /dev/null +++ b/WhiskyKit/Sources/WhiskyKit/PE/Magic.swift @@ -0,0 +1,40 @@ +// +// PortableExecutable+Magic.swift +// WhiskyKit +// +// This file is part of Whisky. +// +// Whisky 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. +// +// Whisky 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 Whisky. +// If not, see https://www.gnu.org/licenses/. +// + +import Foundation + +extension PEFile { + public enum Magic: UInt16, Hashable, Equatable, CustomStringConvertible { + case unknown = 0x0 + case pe32 = 0x10b + case pe32Plus = 0x20b + + // MARK: - CustomStringConvertible + + public var description: String { + switch self { + case .unknown: + return "unknown" + case .pe32: + return "PE32" + case .pe32Plus: + return "PE32+" + } + } + } +} diff --git a/WhiskyKit/Sources/WhiskyKit/PE/OptionalHeader.swift b/WhiskyKit/Sources/WhiskyKit/PE/OptionalHeader.swift new file mode 100644 index 00000000..d48f116d --- /dev/null +++ b/WhiskyKit/Sources/WhiskyKit/PE/OptionalHeader.swift @@ -0,0 +1,144 @@ +// +// PortableExecutable+OptionalHeader.swift +// WhiskyKit +// +// This file is part of Whisky. +// +// Whisky 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. +// +// Whisky 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 Whisky. +// If not, see https://www.gnu.org/licenses/. +// + +import Foundation + +extension PEFile { + /// Optional Header + /// + /// https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#optional-header-image-only + public struct OptionalHeader: Hashable, Equatable { + // Standard Fields + + public let magic: Magic + public let majorLinkerVersion: UInt8 + public let minorLinkerVersion: UInt8 + public let sizeOfCode: UInt32 + public let sizeOfInitializedData: UInt32 + public let sizeOfUninitializedData: UInt32 + public let addressOfEntryPoint: UInt32 + public let baseOfCode: UInt32 + public let baseOfData: UInt32? + + // Windows-Specific Fields + + public let imageBase: UInt64 + public let sectionAlignment: UInt32 + public let fileAlignment: UInt32 + public let majorOperatingSystemVersion: UInt16 + public let minorOperatingSystemVersion: UInt16 + public let majorImageVersion: UInt16 + public let minorImageVersion: UInt16 + public let majorSubsystemVersion: UInt16 + public let minorSubsystemVersion: UInt16 + public let win32VersionValue: UInt32 + public let sizeOfImage: UInt32 + public let sizeOfHeaders: UInt32 + public let checkSum: UInt32 + public let subsystem: UInt16 + public let dllCharacteristics: UInt16 + public let sizeOfStackReserve: UInt32 + public let sizeOfStackCommit: UInt32 + public let sizeOfHeapReserve: UInt32 + public let sizeOfHeapCommit: UInt32 + public let loaderFlags: UInt32 + public let numberOfRvaAndSizes: UInt32 + + // swiftlint:disable:next function_body_length + init?(handle: FileHandle, offset: UInt64) { + var offset = offset + let rawMagic = handle.extract(UInt16.self, offset: offset) ?? 0 + let magic = Magic(rawValue: rawMagic) ?? .unknown + self.magic = magic + offset += 2 + self.majorLinkerVersion = handle.extract(UInt8.self, offset: offset) ?? 0 + offset += 1 + self.minorLinkerVersion = handle.extract(UInt8.self, offset: offset) ?? 0 + offset += 1 + self.sizeOfCode = handle.extract(UInt32.self, offset: offset) ?? 0 + offset += 4 + self.sizeOfInitializedData = handle.extract(UInt32.self, offset: offset) ?? 0 + offset += 4 + self.sizeOfUninitializedData = handle.extract(UInt32.self, offset: offset) ?? 0 + offset += 4 + self.addressOfEntryPoint = handle.extract(UInt32.self, offset: offset) ?? 0 + offset += 4 + self.baseOfCode = handle.extract(UInt32.self, offset: offset) ?? 0 + offset += 4 + + switch magic { + case .pe32Plus: + // PE32+ does not contain this field, following BaseOfCode is a larger ImageBase instead. + self.baseOfData = nil + + // PE32+ images have a 8 byte ImageBase field + self.imageBase = handle.extract(UInt64.self, offset: offset) ?? 0 + offset += 8 + default: + // PE32 contains this additional field, following BaseOfCode. + self.baseOfData = handle.extract(UInt32.self, offset: offset) ?? 0 + offset += 4 + + // PE32 images have a 4 byte ImageBase field + let imageBase = handle.extract(UInt32.self, offset: offset) ?? 0 + self.imageBase = UInt64(imageBase) + offset += 4 + } + + self.sectionAlignment = handle.extract(UInt32.self, offset: offset) ?? 0 + offset += 4 + self.fileAlignment = handle.extract(UInt32.self, offset: offset) ?? 0 + offset += 4 + self.majorOperatingSystemVersion = handle.extract(UInt16.self, offset: offset) ?? 0 + offset += 2 + self.minorOperatingSystemVersion = handle.extract(UInt16.self, offset: offset) ?? 0 + offset += 2 + self.majorImageVersion = handle.extract(UInt16.self, offset: offset) ?? 0 + offset += 2 + self.minorImageVersion = handle.extract(UInt16.self, offset: offset) ?? 0 + offset += 2 + self.majorSubsystemVersion = handle.extract(UInt16.self, offset: offset) ?? 0 + offset += 2 + self.minorSubsystemVersion = handle.extract(UInt16.self, offset: offset) ?? 0 + offset += 2 + self.win32VersionValue = handle.extract(UInt32.self, offset: offset) ?? 0 + offset += 4 + self.sizeOfImage = handle.extract(UInt32.self, offset: offset) ?? 0 + offset += 4 + self.sizeOfHeaders = handle.extract(UInt32.self, offset: offset) ?? 0 + offset += 4 + self.checkSum = handle.extract(UInt32.self, offset: offset) ?? 0 + offset += 4 + self.subsystem = handle.extract(UInt16.self, offset: offset) ?? 0 + offset += 2 + self.dllCharacteristics = handle.extract(UInt16.self, offset: offset) ?? 0 + offset += 2 + self.sizeOfStackReserve = handle.extract(UInt32.self, offset: offset) ?? 0 + offset += 4 + self.sizeOfStackCommit = handle.extract(UInt32.self, offset: offset) ?? 0 + offset += 4 + self.sizeOfHeapReserve = handle.extract(UInt32.self, offset: offset) ?? 0 + offset += 4 + self.sizeOfHeapCommit = handle.extract(UInt32.self, offset: offset) ?? 0 + offset += 4 + self.loaderFlags = handle.extract(UInt32.self, offset: offset) ?? 0 + offset += 4 + self.numberOfRvaAndSizes = handle.extract(UInt32.self, offset: offset) ?? 0 + } + } +} diff --git a/WhiskyKit/Sources/WhiskyKit/PE/PortableExecutable.swift b/WhiskyKit/Sources/WhiskyKit/PE/PortableExecutable.swift index 23413645..7fe0db97 100644 --- a/WhiskyKit/Sources/WhiskyKit/PE/PortableExecutable.swift +++ b/WhiskyKit/Sources/WhiskyKit/PE/PortableExecutable.swift @@ -17,7 +17,9 @@ // import Foundation +#if canImport(AppKit) import AppKit +#endif public struct PEError: Error { public let message: String @@ -25,283 +27,166 @@ public struct PEError: Error { static let invalidPEFile = PEError(message: "Invalid PE file") } -public struct PESection: Hashable { - public let name: String - public let virtualSize: UInt32 - public let virtualAddress: UInt32 - public let sizeOfRawData: UInt32 - public let pointerToRawData: UInt32 - public let pointerToRelocations: UInt32 - public let pointerToLineNumbers: UInt32 - public let numberOfRelocations: UInt16 - public let numberOfLineNumbers: UInt16 - public let characteristics: UInt32 - // public let data: Data? +public enum Architecture: Hashable { + case x32 + case x64 + case unknown - init?(handle: FileHandle, offset: Int) throws { - var offset = offset - try handle.seek(toOffset: UInt64(offset)) - if let nameData = try handle.read(upToCount: 8) { - self.name = String(data: nameData, encoding: .utf8) ?? "" - } else { - self.name = "" + public func toString() -> String? { + switch self { + case .x32: + return "32-bit" + case .x64: + return "64-bit" + default: + return nil } - offset += 8 - self.virtualSize = handle.extract(UInt32.self, offset: offset) ?? 0 - offset += 4 - self.virtualAddress = handle.extract(UInt32.self, offset: offset) ?? 0 - offset += 4 - self.sizeOfRawData = handle.extract(UInt32.self, offset: offset) ?? 0 - offset += 4 - self.pointerToRawData = handle.extract(UInt32.self, offset: offset) ?? 0 - offset += 4 - self.pointerToRelocations = handle.extract(UInt32.self, offset: offset) ?? 0 - offset += 4 - self.pointerToLineNumbers = handle.extract(UInt32.self, offset: offset) ?? 0 - offset += 4 - self.numberOfRelocations = handle.extract(UInt16.self, offset: offset) ?? 0 - offset += 2 - self.numberOfLineNumbers = handle.extract(UInt16.self, offset: offset) ?? 0 - offset += 2 - self.characteristics = handle.extract(UInt32.self, offset: offset) ?? 0 - offset += 4 -// if sizeOfRawData > 0 { -// let dataOffset = Int(pointerToRawData) -// self.data = data.subdata(in: dataOffset.. 0 { + self.optionalHeader = OptionalHeader(handle: fileHandle, offset: offset) + offset += UInt64(coffFileHeader.sizeOfOptionalHeader) + } else { + self.optionalHeader = nil + } - // swiftlint:disable:next function_body_length - init(handle: FileHandle, offset: Int) { - var offset = offset - self.magic = handle.extract(UInt16.self, offset: offset) ?? 0 - offset += 2 - self.majorLinkerVersion = handle.extract(UInt8.self, offset: offset) ?? 0 - offset += 1 - self.minorLinkerVersion = handle.extract(UInt8.self, offset: offset) ?? 0 - offset += 1 - self.sizeOfCode = handle.extract(UInt32.self, offset: offset) ?? 0 - offset += 4 - self.sizeOfInitializedData = handle.extract(UInt32.self, offset: offset) ?? 0 - offset += 4 - self.sizeOfUninitializedData = handle.extract(UInt32.self, offset: offset) ?? 0 - offset += 4 - self.addressOfEntryPoint = handle.extract(UInt32.self, offset: offset) ?? 0 - offset += 4 - self.baseOfCode = handle.extract(UInt32.self, offset: offset) ?? 0 - offset += 4 - self.baseOfData = handle.extract(UInt32.self, offset: offset) ?? 0 - offset += 4 - self.imageBase = handle.extract(UInt32.self, offset: offset) ?? 0 - offset += 4 - self.sectionAlignment = handle.extract(UInt32.self, offset: offset) ?? 0 - offset += 4 - self.fileAlignment = handle.extract(UInt32.self, offset: offset) ?? 0 - offset += 4 - self.majorOperatingSystemVersion = handle.extract(UInt16.self, offset: offset) ?? 0 - offset += 2 - self.minorOperatingSystemVersion = handle.extract(UInt16.self, offset: offset) ?? 0 - offset += 2 - self.majorImageVersion = handle.extract(UInt16.self, offset: offset) ?? 0 - offset += 2 - self.minorImageVersion = handle.extract(UInt16.self, offset: offset) ?? 0 - offset += 2 - self.majorSubsystemVersion = handle.extract(UInt16.self, offset: offset) ?? 0 - offset += 2 - self.minorSubsystemVersion = handle.extract(UInt16.self, offset: offset) ?? 0 - offset += 2 - self.win32VersionValue = handle.extract(UInt32.self, offset: offset) ?? 0 - offset += 4 - self.sizeOfImage = handle.extract(UInt32.self, offset: offset) ?? 0 - offset += 4 - self.sizeOfHeaders = handle.extract(UInt32.self, offset: offset) ?? 0 - offset += 4 - self.checkSum = handle.extract(UInt32.self, offset: offset) ?? 0 - offset += 4 - self.subsystem = handle.extract(UInt16.self, offset: offset) ?? 0 - offset += 2 - self.dllCharacteristics = handle.extract(UInt16.self, offset: offset) ?? 0 - offset += 2 - self.sizeOfStackReserve = handle.extract(UInt32.self, offset: offset) ?? 0 - offset += 4 - self.sizeOfStackCommit = handle.extract(UInt32.self, offset: offset) ?? 0 - offset += 4 - self.sizeOfHeapReserve = handle.extract(UInt32.self, offset: offset) ?? 0 - offset += 4 - self.sizeOfHeapCommit = handle.extract(UInt32.self, offset: offset) ?? 0 - offset += 4 - self.loaderFlags = handle.extract(UInt32.self, offset: offset) ?? 0 - offset += 4 - self.numberOfRvaAndSizes = handle.extract(UInt32.self, offset: offset) ?? 0 + var sections: [Section] = [] + for _ in 0.. String? { - switch self { - case .x32: - return "32-bit" - case .x64: - return "64-bit" + /// The ``Architecture`` of the executable + public var architecture: Architecture { + switch optionalHeader?.magic { + case .pe32: + return .x32 + case .pe32Plus: + return .x64 default: - return nil + return .unknown } } -} -public struct PEFile: Hashable { - public let coffFileHeader: COFFFileHeader - public var resourceSection: ResourceSection? { - do { - return try ResourceSection(handle: handle, - sectionTable: coffFileHeader.sectionTable, - imageBase: coffFileHeader.optionalHeader.imageBase) - } catch { + /// Read the resource section + /// + /// - Parameters: + /// - handle: The `FileHandle` to read the resource table section from. + /// - types: Only read entrys of the given types. Only applies to the root table. Default includes all types. + /// - Returns: The resource table section + private func rsrc(handle: FileHandle, types: [ResourceType] = ResourceType.allCases) -> ResourceDirectoryTable? { + if let resourceSection = sections.first(where: { $0.name == ".rsrc" }) { + return ResourceDirectoryTable( + handle: handle, + pointerToRawData: UInt64(resourceSection.pointerToRawData), + types: types + ) + } else { return nil } } - public var architecture: Architecture { - Architecture(rawValue: coffFileHeader.optionalHeader.magic) ?? .unknown - } - private let handle: FileHandle - public init(url: URL) throws { - self.handle = try FileHandle(forReadingFrom: url) - // Verify it is a PE file by checking for the PE header - let offsetToPEHeader = handle.extract(UInt32.self, offset: 0x3C) ?? 0 - let peHeader = handle.extract(UInt32.self, offset: Int(offsetToPEHeader)) - guard peHeader == 0x4550 else { - throw PEError.invalidPEFile + /// The Resource Directory Table + public var rsrc: ResourceDirectoryTable? { + guard let handle = try? FileHandle(forReadingFrom: url) else { + return nil + } + defer { + try? handle.close() } - coffFileHeader = try COFFFileHeader(handle: handle) + + return rsrc(handle: handle) } + #if canImport(AppKit) + /// The best icon for this executable + /// - Returns: An `NSImage` if there is a renderable icon in the resource directory table public func bestIcon() -> NSImage? { - var icons: [NSImage] = [] - if let resourceSection = resourceSection { - for entries in resourceSection.allEntries where entries.icon.isValid { - icons.append(entries.icon) - } - } else { - print("No resource section") + guard let handle = try? FileHandle(forReadingFrom: url) else { + return nil + } + defer { + try? handle.close() } - if icons.count > 0 { + guard let rsrc = rsrc(handle: handle, types: [.icon]) else { return nil } + let icons = rsrc.allEntries + .compactMap { entry -> NSImage? in + guard let offset = entry.resolveRVA(sections: sections) else { return nil } + let bitmapInfo = BitmapInfoHeader(handle: handle, offset: UInt64(offset)) + if bitmapInfo.size != 40 { + do { + try handle.seek(toOffset: UInt64(offset)) + if let iconData = try handle.read(upToCount: Int(entry.size)) { + if let rep = NSBitmapImageRep(data: iconData) { + let image = NSImage(size: rep.size) + image.addRepresentation(rep) + return image + } + } + } catch { + print("Failed to get icon") + } + } else if bitmapInfo.colorFormat != .unknown { + return bitmapInfo.renderBitmap(handle: handle, offset: UInt64(offset + bitmapInfo.size)) + } + + return nil + } + .filter { $0.isValid } + + if !icons.isEmpty { return icons.max(by: { $0.size.height < $1.size.height }) + } else { + return NSImage() } - - return NSImage() } + #endif } diff --git a/WhiskyKit/Sources/WhiskyKit/PE/RSRC/ResourceDataEntry.swift b/WhiskyKit/Sources/WhiskyKit/PE/RSRC/ResourceDataEntry.swift new file mode 100644 index 00000000..5cad2b3f --- /dev/null +++ b/WhiskyKit/Sources/WhiskyKit/PE/RSRC/ResourceDataEntry.swift @@ -0,0 +1,51 @@ +// +// ResourceDataEntry.swift +// WhiskyKit +// +// This file is part of Whisky. +// +// Whisky 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. +// +// Whisky 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 Whisky. +// If not, see https://www.gnu.org/licenses/. +// + +import Foundation + +/// Each Resource Data entry describes an actual unit of raw data in the Resource Data area +/// +/// https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#resource-data-entry +public struct ResourceDataEntry: Hashable, Equatable { + public let dataRVA: UInt32 + public let size: UInt32 + public let codePage: UInt32 + + init?(handle: FileHandle, offset: UInt64) { + var offset = offset + self.dataRVA = handle.extract(UInt32.self, offset: offset) ?? 0 + offset += 4 + self.size = handle.extract(UInt32.self, offset: offset) ?? 0 + offset += 4 + self.codePage = handle.extract(UInt32.self, offset: offset) ?? 0 + offset += 4 + let reserved = handle.extract(UInt32.self, offset: offset) ?? 0 + offset += 4 + guard reserved == 0 else { return nil } + } + + public func resolveRVA(sections: [PEFile.Section]) -> UInt32? { + sections + .first { section in + section.virtualAddress <= dataRVA && dataRVA < (section.virtualAddress + section.virtualSize) + } + .map { section in + section.pointerToRawData + (dataRVA - section.virtualAddress) + } + } +} diff --git a/WhiskyKit/Sources/WhiskyKit/PE/RSRC/ResourceDirectoryEntry.swift b/WhiskyKit/Sources/WhiskyKit/PE/RSRC/ResourceDirectoryEntry.swift new file mode 100644 index 00000000..4d8399dd --- /dev/null +++ b/WhiskyKit/Sources/WhiskyKit/PE/RSRC/ResourceDirectoryEntry.swift @@ -0,0 +1,52 @@ +// +// ResourceDirectoryEntry.swift +// WhiskyKit +// +// This file is part of Whisky. +// +// Whisky 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. +// +// Whisky 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 Whisky. +// If not, see https://www.gnu.org/licenses/. +// + +import Foundation + +/// The directory entries make up the rows of a table. +/// +/// https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#resource-directory-entries +public enum ResourceDirectoryEntry { + public struct ID { // swiftlint:disable:this type_name + public let type: ResourceType + private let rawOffset: UInt32 + + init(handle: FileHandle, offset: UInt64) { + var offset = offset + let rawType = handle.extract(UInt32.self, offset: offset) ?? 0 + self.type = ResourceType(rawValue: rawType) ?? .unknown + offset += 4 + self.rawOffset = handle.extract(UInt32.self, offset: offset) ?? 0 + offset += 4 + } + + /// Check if the entry is a directory entry + var isDirectory: Bool { + (rawOffset & 0x80000000) != 0 + } + + /// The offset of the entry + var offset: UInt32 { + if isDirectory { + return rawOffset & 0x7FFFFFFF + } else { + return rawOffset + } + } + } +} diff --git a/WhiskyKit/Sources/WhiskyKit/PE/RSRC/ResourceDirectoryTable.swift b/WhiskyKit/Sources/WhiskyKit/PE/RSRC/ResourceDirectoryTable.swift new file mode 100644 index 00000000..c83d7020 --- /dev/null +++ b/WhiskyKit/Sources/WhiskyKit/PE/RSRC/ResourceDirectoryTable.swift @@ -0,0 +1,125 @@ +// +// ResourceDirectoryTable.swift +// WhiskyKit +// +// This file is part of Whisky. +// +// Whisky 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. +// +// Whisky 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 Whisky. +// If not, see https://www.gnu.org/licenses/. +// + +import Foundation +import SemanticVersion + +/// This data structure should be considered the heading of a table, +/// because the table actually consists of directory entries +/// +/// https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#resource-directory-table +public struct ResourceDirectoryTable: Hashable, Equatable { + public let characteristics: UInt32 + public let timeDateStamp: Date + public let version: SemanticVersion + public let numberOfNameEntries: UInt16 + public let numberOfIdEntries: UInt16 + + public let subtables: [ResourceDirectoryTable] + public let entries: [ResourceDataEntry] + + /// Read the Resource Directory Table + /// + /// - Parameters: + /// - fileHandle: The file handle to read the data from. + /// - pointerToRawData: The offset to the Resource Directory Table in the file handle. + /// - types: Only read entrys of the given types. Only applies to the root table. Defaults to `nil`. + init(handle: FileHandle, pointerToRawData: UInt64, types: [ResourceType]?) { + self.init(handle: handle, pointerToRawData: pointerToRawData, offset: 0, types: types) + } + + /// Read the Resource Directory Table + /// + /// - Parameters: + /// - fileHandle: The file handle to read the data from. + /// - pointerToRawData: The offset to the Resource Directory Table in the file handle. + /// - offset: Additional offset to the `pointerToRawData`. + /// Use only for sub-tables. The root-table has the offset 0. + /// - types: Only read entrys of the given types. Only applies to the root table. Defaults to `nil`. + init( + handle: FileHandle, + pointerToRawData: UInt64, + offset initialOffset: UInt64, + types: [ResourceType]? = nil + ) { + var offset = pointerToRawData + initialOffset + self.characteristics = handle.extract(UInt32.self, offset: offset) ?? 0 + offset += 4 + let timeDateStamp = handle.extract(UInt32.self, offset: offset) ?? 0 + self.timeDateStamp = Date(timeIntervalSince1970: TimeInterval(timeDateStamp)) + offset += 4 + let majorVersion = handle.extract(UInt16.self, offset: offset) ?? 0 + offset += 2 + let minorVersion = handle.extract(UInt16.self, offset: offset) ?? 0 + offset += 2 + self.version = SemanticVersion(Int(majorVersion), Int(minorVersion), 0) + let numberOfNameEntries = handle.extract(UInt16.self, offset: offset) ?? 0 + self.numberOfNameEntries = numberOfNameEntries + offset += 2 + let numberOfIdEntries = handle.extract(UInt16.self, offset: offset) ?? 0 + self.numberOfIdEntries = numberOfIdEntries + offset += 2 + + var subtables: [ResourceDirectoryTable] = [] + var entries: [ResourceDataEntry] = [] + + for _ in 0..(_ type: T.Type, offset: Int = 0) -> T? { - do { - try self.seek(toOffset: UInt64(offset)) - if let data = try self.read(upToCount: MemoryLayout.size) { - return data.withUnsafeBytes { $0.loadUnaligned(as: T.self)} - } - } catch { - return nil - } +/// The type of the ``ResourceDirectoryEntry`` +/// +/// Only applicable to ``ResourceDirectoryEntry`` with an ID +public enum ResourceType: UInt32, CaseIterable, Hashable, Equatable { + case unknown + // We only care about icon + case icon = 3 - return nil + public init?(rawValue: UInt32?) { + if let rawValue, let value = ResourceType(rawValue: rawValue) { + self = value + } else { + self = .unknown + } } } diff --git a/WhiskyKit/Sources/WhiskyKit/PE/ResourceSection.swift b/WhiskyKit/Sources/WhiskyKit/PE/ResourceSection.swift deleted file mode 100644 index 09e599c7..00000000 --- a/WhiskyKit/Sources/WhiskyKit/PE/ResourceSection.swift +++ /dev/null @@ -1,213 +0,0 @@ -// -// ResourceSection.swift -// WhiskyKit -// -// This file is part of Whisky. -// -// Whisky 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. -// -// Whisky 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 Whisky. -// If not, see https://www.gnu.org/licenses/. -// - -import Foundation -import AppKit - -public struct ResourceDirectoryEntry: Hashable { - public let id: UInt32 - public let offsetToData: UInt32 - public let offsetToSubdirectory: UInt32 - public let dataIsDirectory: Bool - - init(handle: FileHandle, offset: Int) { - var offset = offset - // Can be name or ID - self.id = handle.extract(UInt32.self, offset: offset) ?? 0 - offset += 4 - self.offsetToData = handle.extract(UInt32.self, offset: offset) ?? 0 - offset += 4 - - self.dataIsDirectory = (offsetToData & 0x80000000) != 0 - self.offsetToSubdirectory = offsetToData & 0x7FFFFFFF - } -} - -public struct ResourceDataEntry: Hashable { - public let dataRVA: UInt32 - public let size: UInt32 - public let codePage: UInt32 - public let reserved: UInt32 - public let icon: NSImage - - init(handle: FileHandle, offset: Int, sectionTable: SectionTable) { - var offset = offset - var icon = NSImage() - self.dataRVA = handle.extract(UInt32.self, offset: offset) ?? 0 - offset += 4 - self.size = handle.extract(UInt32.self, offset: offset) ?? 0 - offset += 4 - self.codePage = handle.extract(UInt32.self, offset: offset) ?? 0 - offset += 4 - self.reserved = handle.extract(UInt32.self, offset: offset) ?? 0 - offset += 4 - - if let offsetToData = ResourceDataEntry.resolveRVA(rva: dataRVA, sectionTable: sectionTable) { - let bitmapInfo = BitmapInfoHeader(handle: handle, offset: Int(offsetToData)) - if bitmapInfo.size != 40 { - do { - try handle.seek(toOffset: UInt64(offsetToData)) - if let iconData = try handle.read(upToCount: Int(size)) { - if let rep = NSBitmapImageRep(data: iconData) { - icon = NSImage(size: rep.size) - icon.addRepresentation(rep) - } - } - } catch { - print("Failed to get icon") - } - } else { - if bitmapInfo.colorFormat != .unknown { - icon = bitmapInfo.renderBitmap(handle: handle, - offset: Int(offsetToData + bitmapInfo.size)) - } - } - } else { - print("Failed to resolve RVA") - } - - self.icon = icon - } - - static func resolveRVA (rva: UInt32, sectionTable: SectionTable) -> UInt32? { - for section in sectionTable.sections { - if section.virtualAddress <= rva && rva < (section.virtualAddress + section.virtualSize) { - let virtualAddress = section.pointerToRawData + (rva - section.virtualAddress) - return virtualAddress - } - } - - return nil - } -} - -public struct ResourceDirectoryTable: Hashable { - public let characteristics: UInt32 - public let timeDateStamp: UInt32 - public let majorVersion: UInt16 - public let minorVersion: UInt16 - public let numberOfNamedEntries: UInt16 - public let numberOfIdEntries: UInt16 - - public let subtables: [ResourceDirectoryTable] - public let entries: [ResourceDataEntry] - - init(handle: FileHandle, - address: Int, - offset: Int, - sectionTable: SectionTable, - entries: inout [ResourceDataEntry], - depth: Int = 0) { - var offset = offset - self.characteristics = handle.extract(UInt32.self, offset: offset) ?? 0 - offset += 4 - self.timeDateStamp = handle.extract(UInt32.self, offset: offset) ?? 0 - offset += 4 - self.majorVersion = handle.extract(UInt16.self, offset: offset) ?? 0 - offset += 2 - self.minorVersion = handle.extract(UInt16.self, offset: offset) ?? 0 - offset += 2 - self.numberOfNamedEntries = handle.extract(UInt16.self, offset: offset) ?? 0 - offset += 2 - self.numberOfIdEntries = handle.extract(UInt16.self, offset: offset) ?? 0 - offset += 2 - - var subtables: [ResourceDirectoryTable] = [] - - var numberOfNamedEntriesIterated = 0 - for _ in 0.. Program? { - var offset: Int = 0 + var offset: UInt64 = 0 let headerSize = handle.extract(UInt32.self) ?? 0 // Move past headerSize and linkCLSID offset += 4 + 16 let rawLinkFlags = handle.extract(UInt32.self, offset: offset) ?? 0 let linkFlags = LinkFlags(rawValue: rawLinkFlags) - offset = Int(headerSize) + offset = UInt64(headerSize) if linkFlags.contains(.hasLinkTargetIDList) { // We don't need this section so just get the size, and skip ahead - offset += Int(handle.extract(UInt16.self, offset: offset) ?? 0) + 2 + offset += UInt64(handle.extract(UInt16.self, offset: offset) ?? 0) + 2 } if linkFlags.contains(.hasLinkInfo) { @@ -61,7 +61,7 @@ public struct LinkInfo: Hashable { public var linkInfoFlags: LinkInfoFlags public var program: Program? - public init(handle: FileHandle, bottle: Bottle, offset: inout Int) { + public init(handle: FileHandle, bottle: Bottle, offset: inout UInt64) { let startOfSection = offset let linkInfoSize = handle.extract(UInt32.self, offset: offset) ?? 0 @@ -77,7 +77,7 @@ public struct LinkInfo: Hashable { if linkInfoHeaderSize >= 0x00000024 { offset += 20 let localBasePathOffsetUnicode = handle.extract(UInt32.self, offset: offset) ?? 0 - let localPathOffset = startOfSection + Int(localBasePathOffsetUnicode) + let localPathOffset = startOfSection + UInt64(localBasePathOffsetUnicode) program = getProgram(handle: handle, offset: localPathOffset, @@ -86,7 +86,7 @@ public struct LinkInfo: Hashable { } else { offset += 8 let localBasePathOffset = handle.extract(UInt32.self, offset: offset) ?? 0 - let localPathOffset = startOfSection + Int(localBasePathOffset) + let localPathOffset = startOfSection + UInt64(localBasePathOffset) program = getProgram(handle: handle, offset: localPathOffset, @@ -95,12 +95,12 @@ public struct LinkInfo: Hashable { } } - offset = startOfSection + Int(linkInfoSize) + offset = startOfSection + UInt64(linkInfoSize) } - func getProgram(handle: FileHandle, offset: Int, bottle: Bottle, unicode: Bool) -> Program? { + func getProgram(handle: FileHandle, offset: UInt64, bottle: Bottle, unicode: Bool) -> Program? { do { - try handle.seek(toOffset: UInt64(offset)) + try handle.seek(toOffset: offset) if let pathData = try handle.readToEnd() { if let nullRange = pathData.firstIndex(of: 0) { let encoding: String.Encoding = unicode ? .utf16 : .windowsCP1254 From f26b91483bf61035b9f362901265801959925d33 Mon Sep 17 00:00:00 2001 From: David Walter Date: Fri, 1 Dec 2023 18:05:54 +0100 Subject: [PATCH 2/3] Async BestIcon --- Whisky/Views/Bottle/Pins/PinView.swift | 15 ++++++++------- Whisky/Views/Programs/ProgramView.swift | 13 ++++++++----- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/Whisky/Views/Bottle/Pins/PinView.swift b/Whisky/Views/Bottle/Pins/PinView.swift index 6d71c813..aa28b884 100644 --- a/Whisky/Views/Bottle/Pins/PinView.swift +++ b/Whisky/Views/Bottle/Pins/PinView.swift @@ -25,7 +25,7 @@ struct PinView: View { @State var pin: PinnedProgram @Binding var path: NavigationPath - @State private var image: NSImage? + @State private var image: Image? @State private var showRenameSheet = false @State private var name: String = "" @State private var opening: Bool = false @@ -34,7 +34,7 @@ struct PinView: View { VStack { Group { if let image = image { - Image(nsImage: image) + image .resizable() } else { Image(systemName: "app.dashed") @@ -78,13 +78,14 @@ struct PinView: View { .sheet(isPresented: $showRenameSheet) { PinRenameView(name: $name) } - .onAppear { + .task { name = pin.name - Task.detached { @MainActor in - if let peFile = program.peFile { - image = peFile.bestIcon() - } + guard let peFile = program.peFile else { return } + let task = Task.detached { + guard let image = peFile.bestIcon() else { return nil } + return Image(nsImage: image) } + self.image = await task.value } .onChange(of: name) { if let index = bottle.settings.pins.firstIndex(where: { $0.url == pin.url }) { diff --git a/Whisky/Views/Programs/ProgramView.swift b/Whisky/Views/Programs/ProgramView.swift index 73d3d953..5902c74b 100644 --- a/Whisky/Views/Programs/ProgramView.swift +++ b/Whisky/Views/Programs/ProgramView.swift @@ -22,7 +22,7 @@ import UniformTypeIdentifiers struct ProgramView: View { @ObservedObject var program: Program - @State var image: NSImage? + @State var image: Image? @State var programLoading: Bool = false @AppStorage("configSectionExapnded") private var configSectionExpanded: Bool = true @@ -94,7 +94,7 @@ struct ProgramView: View { ToolbarItem(placement: .navigation) { Group { if let icon = image { - Image(nsImage: icon) + icon .resizable() .frame(width: 25, height: 25) } else { @@ -106,10 +106,13 @@ struct ProgramView: View { .padding(.trailing, 5) } } - .onAppear { - if let peFile = program.peFile { - image = peFile.bestIcon() + .task { + guard let peFile = program.peFile else { return } + let task = Task.detached { + guard let image = peFile.bestIcon() else { return nil } + return Image(nsImage: image) } + self.image = await task.value } } } From 3f938200d152ad4dbc9c9bd821cb4f888a8f6894 Mon Sep 17 00:00:00 2001 From: David Walter Date: Fri, 8 Dec 2023 19:47:48 +0100 Subject: [PATCH 3/3] Remove canImport(AppKit) --- WhiskyKit/Sources/WhiskyKit/PE/PortableExecutable.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/WhiskyKit/Sources/WhiskyKit/PE/PortableExecutable.swift b/WhiskyKit/Sources/WhiskyKit/PE/PortableExecutable.swift index 7fe0db97..5f9d29cb 100644 --- a/WhiskyKit/Sources/WhiskyKit/PE/PortableExecutable.swift +++ b/WhiskyKit/Sources/WhiskyKit/PE/PortableExecutable.swift @@ -17,9 +17,7 @@ // import Foundation -#if canImport(AppKit) import AppKit -#endif public struct PEError: Error { public let message: String @@ -145,7 +143,6 @@ public struct PEFile: Hashable, Equatable { return rsrc(handle: handle) } - #if canImport(AppKit) /// The best icon for this executable /// - Returns: An `NSImage` if there is a renderable icon in the resource directory table public func bestIcon() -> NSImage? { @@ -166,7 +163,7 @@ public struct PEFile: Hashable, Equatable { try handle.seek(toOffset: UInt64(offset)) if let iconData = try handle.read(upToCount: Int(entry.size)) { if let rep = NSBitmapImageRep(data: iconData) { - let image = NSImage(size: rep.size) + let image = NSImage(size: rep.size) image.addRepresentation(rep) return image } @@ -188,5 +185,4 @@ public struct PEFile: Hashable, Equatable { return NSImage() } } - #endif }