Skip to content

Commit

Permalink
Add the ability to parse user-defined resource types.
Browse files Browse the repository at this point in the history
This is important for extracting resources from applications like After Dark and the easter eggs. :)
  • Loading branch information
npjg committed Jul 12, 2023
1 parent 77a812a commit 4873c06
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 70 deletions.
6 changes: 3 additions & 3 deletions nefile/nefile.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def __init__(self, stream):

## Models a New Executable file.
class NE:
def __init__(self, filepath: str = None, stream = None):
def __init__(self, filepath: str = None, stream = None, user_defined_resource_parsers = {}):
# MAP THE FILE DATA.
# A filepath can be provided to open a file from disk, or an in-memory binary stream can be provided.
# However, providing both of these is an error.
Expand Down Expand Up @@ -49,7 +49,7 @@ def __init__(self, filepath: str = None, stream = None):

# READ THE RESOURCE TABLE.
self.stream.seek(self.header.resource_table_offset)
self.resource_table = resource_table.ResourceTable(self.stream)
self.resource_table = resource_table.ResourceTable(self.stream, user_defined_resource_parsers)

@property
def executable_name(self):
Expand All @@ -58,7 +58,7 @@ def executable_name(self):
## Exports all the resources in this executable.
def export_resources(self, directory_path):
for resource_type_code, resource_type in self.resource_table.resources.items():
resource_type_string: str = resource_type_code.name if isinstance(resource_type_code, resource_table.ResourceType) else resource_type_code
resource_type_string: str = resource_type_code.name if isinstance(resource_type_code, Enum) else resource_type_code
for resource_id, resource in resource_type.items():
export_filename = f'{self.executable_name}-{resource_type_string}-{resource_id}'
export_filepath = os.path.join(directory_path, export_filename)
Expand Down
151 changes: 84 additions & 67 deletions nefile/resource_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from .resources.bitmap import Bitmap
from .resources.icon import GroupIcon, Icon
from .resources.cursor import Cursor
from .resources.application_defined_data import ApplicationDefinedData
from .resources.string import StringTable

Expand Down Expand Up @@ -34,11 +35,32 @@ class ResourceType(Enum):
def has_value(cls, value):
return value in (val.value for val in cls.__members__.values())

## The resource table follows the segment table and contains entries
## for each resource type and resource in this NE stream.
## Resource data itself is lazily loaded; the resources are only loaded when requested.

## Declares each of the resource types and resources in this NE stream.
## The resource declarations are read immediately, but the resource data
## is only parsed when requested.
class ResourceTable:
def __init__(self, stream):
BUILT_IN_RESOURCE_PARSERS = {
ResourceType.RT_CURSOR: Cursor,
ResourceType.RT_GROUP_ICON: GroupIcon,
ResourceType.RT_STRING: StringTable
}

## Reads a resource table from a binary stream.
## \param[in] stream - The binary stream positioned at the resource table start.
## \param[in] user_defined_resource_parsers - A dict that maps resource type IDs
## to resource parser classes.
def __init__(self, stream, user_defined_resource_parsers):
# DEFINE THE RESOURCE PARSERS.
# TODO: Support more built-in resources.
self.resource_parsers = {
ResourceType.RT_CURSOR: Cursor,
ResourceType.RT_GROUP_ICON: GroupIcon,
ResourceType.RT_STRING: StringTable
}
self.resource_parsers.update(user_defined_resource_parsers)

# READ THE RESORUCE METADATA.
self.stream = stream
self._resources = None
self.resource_table_start_offset = stream.tell()
Expand All @@ -54,80 +76,70 @@ def __init__(self, stream):
self.resource_type_tables.update({resource_dictionary_key: resource_type})
resource_type = ResourceTypeTable(stream, self.alignment_shift_count, self.resource_table_start_offset)

## Parses the resources in this resource table.
## The resources are always parsed in this order:
## - Built-in types (ResourceType) in numerical order.
## - All other types in the order they appear in the file.
##
## This ordering helps resolve dependencies within and among
## these groups. Built-in types with higher numerical order (like
## RT_GROUPICON) can depend on built-in types with lower numerical
## order (like RT_ICON). And user-defined types can depend on built-
## in types, and so forth.
##
## Built-in and user-defined types are parsed with the provided classes,
## any any other types have raw resource data read for processing later.
@property
def resources(self):
# CHECK IF THE RESOURCES HAVE PREVIOUSLY BEEN PARSED.
# Parsing resources can be expensive, so we cache the
# parsing results after parsing the first time.
if self._resources is not None:
# RETURN THE CACHED RESOURCE DICTIONARY.
return self._resources

# PROCESS RESOURCE TYPES THAT REQUIRE PRE-PROCESSING.
# These resource types must be in the resource dictionary
# since they might be referenced at any time by group resources,
# and it is not guaranteed that group resources are read before
# the individual resources.
self._resources = {
ResourceType.RT_ICON: self._parse_icons(),
ResourceType.RT_CURSOR: self._parse_cursors()
}

self._resources = {}

# PARSE RESOURCES FROM THE BUILT-IN RESOURCE TYPES.
# This loop guarantees the resource types are processed in definition order.
for resource_type in ResourceType:
resource_type_table = self.resource_type_tables.get(resource_type)
if resource_type_table is not None:
resources = self._parse_resources(resource_type_table)
self._resources.update({resource_type_table.type_code: resources})

# PARSE RESOURCES FROM ALL OTHER RESOURCE TYPES.
# THese resource types are processed as they appear in the file.
for resource_type_table in self.resource_type_tables.values():
resource_pre_parsed = resource_type_table.type_code == ResourceType.RT_ICON or \
resource_type_table.type_code == ResourceType.RT_CURSOR
if resource_pre_parsed:
continue

# PARSE THE RESOURCES OF THIS TYPE.
current_type_resources = {}
for resource_declaration in resource_type_table.resource_declarations:
# PARSE THIS INDIVIDUAL RESOURCE.
resource = self._parse_other_resource(resource_declaration, resource_type_table.type_code)
current_type_resources.update({resource_declaration.id: resource})

self._resources.update({resource_type_table.type_code: current_type_resources})
return self._resources
resource_already_parsed = ResourceType.has_value(resource_type_table.type_code)
if not resource_already_parsed:
resources = self._parse_resources(resource_type_table)
self._resources.update({resource_type_table.type_code: resources})

## Finds a resource by
def find_resource_by_id(self, resource_id, type_id):
return self.resources.get(type_id, {}).get(resource_id, None)
# RETURN THE PARSED RESOURCES.
return self._resources

## Constructs the icons in this resource table.
## These must be constructed first because the group resources refer to them.
def _parse_icons(self):
icon_resource_type_table = self.resource_type_tables.get(ResourceType.RT_ICON, None)
icon_resources = {}
if icon_resource_type_table is None:
return icon_resources
## Passes the actual data referenced by each resource declaration
## to the appropriate parsing class and returns the result.
def _parse_resources(self, resource_type_table):
# GET THE PARSER CLASS FOR THIS RESOURCE TYPE.
defined_resource_parser = self.resource_parsers.get(resource_type_table.type_code, ApplicationDefinedData)

for resource_declaration in icon_resource_type_table.resource_declarations:
# PARSE THE RESOURCES OF THIS TYPE.
resources = {}
for resource_declaration in resource_type_table.resource_declarations:
self.stream.seek(resource_declaration.data_start_offset)
icon = Icon(self.stream, resource_declaration, self)
icon_resources.update({resource_declaration.id: icon})
return icon_resources

def _parse_cursors(self):
cursor_resource_type_table = self.resource_type_tables.get(ResourceType.RT_CURSOR, None)
cursor_resources = {}
if cursor_resource_type_table is None:
return cursor_resources
resource = defined_resource_parser(self.stream, resource_declaration, self)
resources.update({resource_declaration.id: resource})
return resources

for resource_declaration in cursor_resource_type_table.resource_declarations:
self.stream.seek(resource_declaration.data_start_offset)
cursor = Cursor(self.stream, resource_declaration, self)
cursor_resources.update({resource_declaration.id: cursor})
return cursor_resources

def _parse_other_resource(self, resource_declaration, resource_type_code):
self.stream.seek(resource_declaration.data_start_offset)
if resource_type_code == ResourceType.RT_BITMAP:
return Bitmap(self.stream, resource_declaration, self)
elif resource_type_code == ResourceType.RT_GROUP_ICON:
return GroupIcon(self.stream, resource_declaration, self)
elif resource_type_code == ResourceType.RT_STRING:
return StringTable(self.stream, resource_declaration, self)
else:
return ApplicationDefinedData(self.stream, resource_declaration, self)
## Finds a resource by its type and resource ID.
## The type is required because two resources with different types
## can have the same resource ID.
def find_resource_by_id(self, resource_id, type_id):
return self.resources.get(type_id, {}).get(resource_id, None)

## Declares each of the resource types stored in this stream,
## Declares each of the resource types stored in this stream.
## Resource data is not actually accessible until the resources are parsed.
class ResourceTypeTable:
def __init__(self, stream, alignment_shift_count, resource_table_start_offset):
# READ THE RESOURCE TYPE INFORMATION.
Expand Down Expand Up @@ -162,7 +174,8 @@ def __init__(self, stream, alignment_shift_count, resource_table_start_offset):
resource_declaration = ResourceDeclaration(stream, alignment_shift_count, resource_table_start_offset)
self.resource_declarations.append(resource_declaration)

## Reads a resource stored in the stream.
## Declares a resource stored in the stream.
## Resource data is not actually accessible until the resources are parsed.
class ResourceDeclaration:
class ResourceFlags(IntFlag):
MOVEABLE = 0x0010,
Expand Down Expand Up @@ -195,6 +208,10 @@ def __init__(self, stream, alignment_shift_count, resource_table_start_offset):
# TODO: Document this, if it can be documented.
self.reserved = stream.read(4)

@property
def data_end_offset(self):
return self.data_start_offset + self.resource_length_in_bytes

## Reads a resource name or type string by seeking to the correct offset.
## These strings are Pascal strings - case-sensitive and not null-terminated.
class ResourceString:
Expand Down

0 comments on commit 4873c06

Please sign in to comment.