mirror of
https://github.com/momo5502/emulator.git
synced 2026-01-18 11:13:57 +00:00
Parse PE icons
This commit is contained in:
15
page/package-lock.json
generated
15
page/package-lock.json
generated
@@ -28,6 +28,7 @@
|
||||
"flatbuffers": "^25.2.10",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide-react": "^0.503.0",
|
||||
"pe-library": "^1.0.1",
|
||||
"react": "^19.0.0",
|
||||
"react-bootstrap-icons": "^1.11.5",
|
||||
"react-dom": "^19.0.0",
|
||||
@@ -4515,6 +4516,20 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pe-library": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pe-library/-/pe-library-1.0.1.tgz",
|
||||
"integrity": "sha512-nh39Mo1eGWmZS7y+mK/dQIqg7S1lp38DpRxkyoHf0ZcUs/HDc+yyTjuOtTvSMZHmfSLuSQaX945u05Y2Q6UWZg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14",
|
||||
"npm": ">=7"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jet2jet"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"flatbuffers": "^25.2.10",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide-react": "^0.503.0",
|
||||
"pe-library": "^1.0.1",
|
||||
"react": "^19.0.0",
|
||||
"react-bootstrap-icons": "^1.11.5",
|
||||
"react-dom": "^19.0.0",
|
||||
|
||||
@@ -36,9 +36,11 @@ type CreateFolderHandler = () => void;
|
||||
type RemoveElementHandler = (element: FolderElement) => void;
|
||||
type RenameElementHandler = (element: FolderElement) => void;
|
||||
type AddFilesHandler = () => void;
|
||||
type IconReader = (element: FolderElement) => string | null;
|
||||
|
||||
export interface FolderProps {
|
||||
elements: FolderElement[];
|
||||
iconReader: IconReader;
|
||||
clickHandler: ClickHandler;
|
||||
createFolderHandler: CreateFolderHandler;
|
||||
removeElementHandler: RemoveElementHandler;
|
||||
@@ -54,7 +56,22 @@ function elementComparator(e1: FolderElement, e2: FolderElement) {
|
||||
return e1.name.localeCompare(e2.name);
|
||||
}
|
||||
|
||||
function getIcon(element: FolderElement, className: string = "") {
|
||||
function getIcon(
|
||||
element: FolderElement,
|
||||
iconReader: IconReader,
|
||||
className: string = "",
|
||||
) {
|
||||
const icon = iconReader(element);
|
||||
if (icon) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="w-full h-full flex items-center">
|
||||
<img className="rounded-sm" src={icon} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
switch (element.type) {
|
||||
case FolderElementType.File:
|
||||
if (element.name.endsWith(".dll")) {
|
||||
@@ -75,18 +92,18 @@ function getIcon(element: FolderElement, className: string = "") {
|
||||
}
|
||||
}
|
||||
|
||||
function renderIcon(element: FolderElement) {
|
||||
function renderIcon(element: FolderElement, iconReader: IconReader) {
|
||||
let className = "w-6 h-6 flex-1";
|
||||
return getIcon(element, className);
|
||||
return getIcon(element, iconReader, className);
|
||||
}
|
||||
|
||||
function renderElement(element: FolderElement, clickHandler: ClickHandler) {
|
||||
function renderElement(element: FolderElement, props: FolderProps) {
|
||||
return (
|
||||
<div
|
||||
onClick={() => clickHandler(element)}
|
||||
onClick={() => props.clickHandler(element)}
|
||||
className="folder-element select-none flex flex-col gap-2 items-center text-center text-xs p-2 m-2 w-25 h-18 rounded-lg border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50"
|
||||
>
|
||||
{renderIcon(element)}
|
||||
{renderIcon(element, props.iconReader)}
|
||||
<span className="whitespace-nowrap text-ellipsis overflow-hidden w-20">
|
||||
{element.name}
|
||||
</span>
|
||||
@@ -96,7 +113,7 @@ function renderElement(element: FolderElement, clickHandler: ClickHandler) {
|
||||
|
||||
function renderElementWithContext(element: FolderElement, props: FolderProps) {
|
||||
if (element.name == "..") {
|
||||
return renderElement(element, props.clickHandler);
|
||||
return renderElement(element, props);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -104,7 +121,7 @@ function renderElementWithContext(element: FolderElement, props: FolderProps) {
|
||||
<ContextMenuTrigger>
|
||||
<Tooltip delayDuration={700}>
|
||||
<TooltipTrigger asChild>
|
||||
{renderElement(element, props.clickHandler)}
|
||||
{renderElement(element, props)}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{element.name}</p>
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from "@/components/ui/breadcrumb";
|
||||
|
||||
import { HouseFill } from "react-bootstrap-icons";
|
||||
import { parsePeIcon } from "./pe-icon-parser";
|
||||
|
||||
export interface FilesystemExplorerProps {
|
||||
filesystem: Filesystem;
|
||||
@@ -156,6 +157,32 @@ function selectFiles(): Promise<FileList> {
|
||||
});
|
||||
}
|
||||
|
||||
interface IconCache {
|
||||
get: (file: string) => string | null;
|
||||
set: (file: string, data: string | null) => void;
|
||||
}
|
||||
|
||||
function getPeIcon(
|
||||
filesystem: Filesystem,
|
||||
file: string,
|
||||
cache: Map<string, string | null>,
|
||||
) {
|
||||
if (!file || !file.endsWith(".exe")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cachedValue = cache.get(file);
|
||||
if (cachedValue) {
|
||||
return cachedValue;
|
||||
}
|
||||
|
||||
const data = filesystem.readFile(file);
|
||||
const icon = parsePeIcon(data);
|
||||
cache.set(file, icon);
|
||||
|
||||
return icon;
|
||||
}
|
||||
|
||||
interface BreadcrumbElement {
|
||||
node: React.ReactNode;
|
||||
targetPath: string[];
|
||||
@@ -182,6 +209,8 @@ export class FilesystemExplorer extends React.Component<
|
||||
FilesystemExplorerProps,
|
||||
FilesystemExplorerState
|
||||
> {
|
||||
private iconCache: Map<string, string | null>;
|
||||
|
||||
constructor(props: FilesystemExplorerProps) {
|
||||
super(props);
|
||||
|
||||
@@ -189,6 +218,8 @@ export class FilesystemExplorer extends React.Component<
|
||||
this._uploadFiles = this._uploadFiles.bind(this);
|
||||
this._onElementSelect = this._onElementSelect.bind(this);
|
||||
|
||||
this.iconCache = new Map();
|
||||
|
||||
this.state = {
|
||||
path: this.props.path,
|
||||
createFolder: false,
|
||||
@@ -556,6 +587,13 @@ export class FilesystemExplorer extends React.Component<
|
||||
this.setState({ renameFile: e.name })
|
||||
}
|
||||
addFilesHandler={this._onAddFiles}
|
||||
iconReader={(e) =>
|
||||
getPeIcon(
|
||||
this.props.filesystem,
|
||||
makeFullPathWithState(this.state, e.name),
|
||||
this.iconCache,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -82,6 +82,10 @@ export class Filesystem {
|
||||
this.idbfs.FS.writeFile(file.name, buffer);
|
||||
}
|
||||
|
||||
readFile(file: string): Uint8Array {
|
||||
return this.idbfs.FS.readFile(file);
|
||||
}
|
||||
|
||||
async storeFiles(files: FileWithData[]) {
|
||||
files.forEach((f) => {
|
||||
this._storeFile(f);
|
||||
|
||||
232
page/src/pe-icon-parser.tsx
Normal file
232
page/src/pe-icon-parser.tsx
Normal file
@@ -0,0 +1,232 @@
|
||||
import * as PE from "pe-library";
|
||||
|
||||
function patchExeFile(exe: PE.NtExecutable) {
|
||||
// The PE library doesn't support parsing resources if other sections follow
|
||||
// This might make sense, as the library will have issues rewriting the PE file.
|
||||
// As we only care about parsing though, just kill the other sections.
|
||||
const rsrc = exe.getSectionByEntry(PE.Format.ImageDirectoryEntry.Resource);
|
||||
const orig = exe.getAllSections.bind(exe);
|
||||
exe.getAllSections = function () {
|
||||
let x = { skip: false };
|
||||
return orig().filter((s) => {
|
||||
if (x.skip) {
|
||||
return false;
|
||||
}
|
||||
if (s == rsrc) {
|
||||
x.skip = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function arrayBufferToBase64(bytes: Uint8Array) {
|
||||
let binary = "";
|
||||
const len = bytes.byteLength;
|
||||
for (let i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
function isPng(buffer: Uint8Array) {
|
||||
if (buffer.length < 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return buffer[1] === 80 && buffer[2] === 78 && buffer[3] === 71;
|
||||
}
|
||||
|
||||
function generateDataURL(arrayBuffer: Uint8Array, contentType: string) {
|
||||
const base64 = arrayBufferToBase64(arrayBuffer);
|
||||
return `data:${contentType};base64,${base64}`;
|
||||
}
|
||||
|
||||
interface IconEntry {
|
||||
width: number;
|
||||
height: number;
|
||||
colorCount: number;
|
||||
reserved: number;
|
||||
planes: number;
|
||||
bitCount: number;
|
||||
bytesInRes: number;
|
||||
id: number;
|
||||
}
|
||||
|
||||
interface IconGroup {
|
||||
reserved: number;
|
||||
type: number;
|
||||
icons: IconEntry[];
|
||||
}
|
||||
|
||||
function writeUint8(buffer: Uint8Array, offset: number, value: number) {
|
||||
buffer[offset] = value;
|
||||
}
|
||||
|
||||
function writeUint16(buffer: Uint8Array, offset: number, value: number) {
|
||||
writeUint8(buffer, offset + 0, value & 0xff);
|
||||
writeUint8(buffer, offset + 1, (value >> 8) & 0xff);
|
||||
}
|
||||
|
||||
function writeUint32(buffer: Uint8Array, offset: number, value: number) {
|
||||
writeUint16(buffer, offset + 0, value & 0xffff);
|
||||
writeUint16(buffer, offset + 2, (value >> 16) & 0xffff);
|
||||
}
|
||||
|
||||
function readUInt8(buffer: Uint8Array, offset: number) {
|
||||
return buffer[offset];
|
||||
}
|
||||
|
||||
function readUInt16(buffer: Uint8Array, offset: number) {
|
||||
return readUInt8(buffer, offset) | (readUInt8(buffer, offset + 1) << 8);
|
||||
}
|
||||
|
||||
function readUInt32(buffer: Uint8Array, offset: number) {
|
||||
return readUInt16(buffer, offset) | (readUInt16(buffer, offset + 2) << 16);
|
||||
}
|
||||
|
||||
function parseIconGroup(buffer: Uint8Array): IconGroup {
|
||||
const reserved = readUInt16(buffer, 0);
|
||||
const type = readUInt16(buffer, 2);
|
||||
const count = readUInt16(buffer, 4);
|
||||
|
||||
const icons: IconEntry[] = [];
|
||||
|
||||
for (let i = 0; i < count; ++i) {
|
||||
const start = 6 + i * 14;
|
||||
const width = readUInt8(buffer, start + 0);
|
||||
const height = readUInt8(buffer, start + 1);
|
||||
const colorCount = readUInt8(buffer, start + 2);
|
||||
const reserved2 = readUInt8(buffer, start + 3);
|
||||
const planes = readUInt16(buffer, start + 4);
|
||||
const bitCount = readUInt16(buffer, start + 6);
|
||||
const bytesInRes = readUInt32(buffer, start + 8);
|
||||
const id = readUInt16(buffer, start + 12);
|
||||
|
||||
icons.push({
|
||||
width,
|
||||
height,
|
||||
colorCount,
|
||||
reserved: reserved2,
|
||||
planes,
|
||||
bitCount,
|
||||
bytesInRes,
|
||||
id,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
reserved,
|
||||
type,
|
||||
icons,
|
||||
};
|
||||
}
|
||||
|
||||
function mergeArrayBuffers(
|
||||
buffer1: ArrayBuffer,
|
||||
buffer2: ArrayBuffer,
|
||||
): ArrayBuffer {
|
||||
const mergedBuffer = new ArrayBuffer(buffer1.byteLength + buffer2.byteLength);
|
||||
|
||||
const view1 = new Uint8Array(buffer1);
|
||||
const view2 = new Uint8Array(buffer2);
|
||||
const mergedView = new Uint8Array(mergedBuffer);
|
||||
|
||||
mergedView.set(view1, 0);
|
||||
mergedView.set(view2, buffer1.byteLength);
|
||||
|
||||
return mergedBuffer;
|
||||
}
|
||||
|
||||
function generateIcoHeader(icon: IconEntry) {
|
||||
const headerSize = 0x16;
|
||||
const header = new Uint8Array(headerSize);
|
||||
writeUint8(header, 2, 1); // Image type -> ico
|
||||
writeUint8(header, 4, 1); // Image count
|
||||
|
||||
const start = 6;
|
||||
|
||||
writeUint8(header, start + 0, icon.width);
|
||||
writeUint8(header, start + 1, icon.height);
|
||||
writeUint8(header, start + 2, icon.colorCount);
|
||||
|
||||
writeUint16(header, start + 4, icon.planes);
|
||||
writeUint16(header, start + 6, icon.bitCount);
|
||||
writeUint32(header, start + 8, icon.bytesInRes);
|
||||
writeUint32(header, start + 12, headerSize);
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
function isMaxResIcon(icon: IconEntry) {
|
||||
return icon.width == 0 && icon.height == 0;
|
||||
}
|
||||
|
||||
function getBiggestIcon(group: IconGroup) {
|
||||
if (group.icons.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var biggest = group.icons[0];
|
||||
if (isMaxResIcon(biggest)) {
|
||||
return biggest;
|
||||
}
|
||||
|
||||
for (let i = 1; i < group.icons.length; ++i) {
|
||||
let current = group.icons[i];
|
||||
if (isMaxResIcon(current)) {
|
||||
return current;
|
||||
}
|
||||
|
||||
if (current.width * current.height > biggest.width * biggest.height) {
|
||||
biggest = current;
|
||||
}
|
||||
}
|
||||
|
||||
return biggest;
|
||||
}
|
||||
|
||||
function getPeResources(data: Uint8Array) {
|
||||
const exe = PE.NtExecutable.from(data, { ignoreCert: true });
|
||||
patchExeFile(exe);
|
||||
return PE.NtExecutableResource.from(exe, true);
|
||||
}
|
||||
|
||||
function getIconDataUrl(iconEntry: IconEntry, iconData: ArrayBuffer) {
|
||||
let contentType = "image/png";
|
||||
|
||||
if (!isPng(new Uint8Array(iconData))) {
|
||||
contentType = "image/x-icon";
|
||||
|
||||
const header = generateIcoHeader(iconEntry);
|
||||
iconData = mergeArrayBuffers(header, iconData);
|
||||
}
|
||||
|
||||
return generateDataURL(new Uint8Array(iconData), contentType);
|
||||
}
|
||||
|
||||
export function parsePeIcon(data: Uint8Array) {
|
||||
const res = getPeResources(data);
|
||||
const icons = res.entries.filter((e) => e.type == 3);
|
||||
const iconGroups = res.entries.filter((e) => e.type == 14);
|
||||
|
||||
if (iconGroups.length == 0 || icons.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const groupData = new Uint8Array(iconGroups[0].bin);
|
||||
const group = parseIconGroup(groupData);
|
||||
const iconEntry = getBiggestIcon(group);
|
||||
|
||||
if (!iconEntry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const icon = icons.find((i) => i.id == iconEntry.id);
|
||||
if (!icon) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getIconDataUrl(iconEntry, icon.bin);
|
||||
}
|
||||
Reference in New Issue
Block a user