Skip to content

executable

Executable: management of the binary file in itself.

Executable

The executable class is used to interact with the binary file.

It handles access to the binary file itself (not the exported) for reads.

Note: The binary is read only once and stored in memory. This is done for performance purposes but does not cope well with low RAM systems and/or huge binaries.

Parameters:

Name Type Description Default
path Path | str

Path towards the executable file

required
endianness Endianness

How are stored the data

required

Attributes:

Name Type Description
exec_file Path

Path towards the executable file

endianness Endianness

Binary endianness

content bytes

Bytes of the binary

Raises:

Type Description
ValueError

If the file is not found

Source code in bindings/python/quokka/executable.py
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
class Executable:
    """The executable class is used to interact with the binary file.

    It handles access to the binary file itself (not the exported) for reads.

    Note: The binary is read only once and stored in memory. This is done for
    performance purposes but does not cope well with low RAM systems and/or huge
    binaries.

    Arguments:
        path: Path towards the executable file
        endianness: How are stored the data

    Attributes:
        exec_file: Path towards the executable file
        endianness: Binary endianness
        content: Bytes of the binary

    Raises:
        ValueError: If the file is not found
    """

    def __init__(self, path: pathlib.Path|str, endianness: Endianness):
        """Constructor"""
        try:
            with open(path, "rb") as file:
                self.content: bytes = file.read()

        except FileNotFoundError:
            raise ValueError("File not found")

        self.exec_file: pathlib.Path = pathlib.Path(path)
        self.endianness: Endianness = endianness

    def read(self, offset: int, size: int) -> bytes:
        """Read `size` at `offset` in the file.

        This method should not be used directly and considered as part of a private API.
        The preferred method are read_bytes / read_string .

        Arguments:
            offset: File offset
            size: Read size

        Returns:
            The content that has been read

        Raises:
            ValueError: when the value is not in the file
        """
        try:
            return self.content[offset : offset + size]
        except IndexError as exc:
            raise ValueError(f"Content not found at offset {offset}") from exc

    def read_string(self, offset: int, size: int|None = None) -> str:
        """Read a string in the file.

        If the size is not given, Quokka will try to read the string until the
        first null byte. That works only for null-terminated strings.

        If the string is null terminated, remove the trailing 0.

        Arguments:
            offset: String file offset
            size: String size if known.

        Returns:
            The decoded string

        Raises:
          ValueError: If the string is not found nor decoded.
        """

        if size is not None:
            try:
                string = self.read(offset, size).decode("utf-8")
            except UnicodeDecodeError as exc:
                raise ValueError("Unable to read or decode the string.") from exc

        else:
            try:
                null_byte = self.content.index(b"\x00", offset)
            except ValueError as exc:
                raise ValueError(
                    "String is not null-terminated and size was not given"
                ) from exc

            string = self.content[offset:null_byte].decode("utf-8")

        # FIX: When returning a single character string, it does not end with a '\0'
        if len(string) > 1 and string.endswith("\x00"):
            return string[:-1]

        return string

    def read_int(self, offset: int, size: int, signed: bool = False) -> int:
        """Read an integer from the binary.

        Arguments:
            offset: Integer file offset
            size: Integer size in bytes"""
        en = {Endianness.BIG_ENDIAN: "big", Endianness.LITTLE_ENDIAN: "little"}[self.endianness]
        return int.from_bytes(self.read(offset, size), en, signed=signed) # type: ignore

    def read_type_value(self, offset: int, type: TypeT) -> TypeValue:
        """Read the data value based on its type.

        Arguments:
            offset: Data file offset
            data_type: Data type

        Returns:
            The data value

        Raises:
            ValueError: If the type is not supported or the value cannot be read.
        """
        en = {Endianness.BIG_ENDIAN: ">", Endianness.LITTLE_ENDIAN: "<"}[self.endianness]

        try:
            if type.is_base_type:
                match type:
                    case BaseType.FLOAT:
                        return struct.unpack(f"{en}f", self.read_bytes(offset, 4))[0]
                    case BaseType.DOUBLE:
                        return struct.unpack(f"{en}d", self.read_bytes(offset, 8))[0]
                    case _:
                        return self.read_int(offset, type.size)
            elif type.is_array:
                return self.read_bytes(offset, type.size)
            elif type.is_pointer:
                return self.read_int(offset, type.size)
            elif type.is_struct or type.is_union:
                assert isinstance(type, (StructureType, UnionType))
                return self.read_struct(offset, type)
            elif type.is_enum:
                assert isinstance(type, EnumType)
                return self.read_enum(offset, type)
            else:
                assert False, f"Unsupported type {type}"
        except Exception as exc:
            raise ValueError(f"Unable to read value of type {type} at offset {offset}") from exc


    def read_struct(self, offset: int, struct: StructureType | UnionType) -> bytes:
        """Read a struct from the binary.

        Arguments:
            offset: Struct file offset
            type: Struct type"""
        if struct.is_variable_size():
            logging.warning("Cannot read a variable size struct")
            return b""
        # FEATURE: Read a really structure instance
        return self.read_bytes(offset, struct.size)

    def read_enum(self, offset: int, enum: EnumType) -> EnumType:
        # read the underyling enum type
        value = self.read_type_value(offset, enum.base_type)  # type: ignore
        return enum(value) # type: ignore

    def read_bytes(self, offset: int, size: int) -> bytes:
        """Read one (or more) byte(s) in the file at `offset`.

        This is mostly used to read instructions.

        Arguments:
            offset: File offset to read
            size: Number of bytes to read

        Returns:
            The bytes values
        """
        return self.read(offset, size)

__init__(path, endianness)

Constructor

Source code in bindings/python/quokka/executable.py
48
49
50
51
52
53
54
55
56
57
58
def __init__(self, path: pathlib.Path|str, endianness: Endianness):
    """Constructor"""
    try:
        with open(path, "rb") as file:
            self.content: bytes = file.read()

    except FileNotFoundError:
        raise ValueError("File not found")

    self.exec_file: pathlib.Path = pathlib.Path(path)
    self.endianness: Endianness = endianness

read(offset, size)

Read size at offset in the file.

This method should not be used directly and considered as part of a private API. The preferred method are read_bytes / read_string .

Parameters:

Name Type Description Default
offset int

File offset

required
size int

Read size

required

Returns:

Type Description
bytes

The content that has been read

Raises:

Type Description
ValueError

when the value is not in the file

Source code in bindings/python/quokka/executable.py
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
def read(self, offset: int, size: int) -> bytes:
    """Read `size` at `offset` in the file.

    This method should not be used directly and considered as part of a private API.
    The preferred method are read_bytes / read_string .

    Arguments:
        offset: File offset
        size: Read size

    Returns:
        The content that has been read

    Raises:
        ValueError: when the value is not in the file
    """
    try:
        return self.content[offset : offset + size]
    except IndexError as exc:
        raise ValueError(f"Content not found at offset {offset}") from exc

read_bytes(offset, size)

Read one (or more) byte(s) in the file at offset.

This is mostly used to read instructions.

Parameters:

Name Type Description Default
offset int

File offset to read

required
size int

Number of bytes to read

required

Returns:

Type Description
bytes

The bytes values

Source code in bindings/python/quokka/executable.py
188
189
190
191
192
193
194
195
196
197
198
199
200
def read_bytes(self, offset: int, size: int) -> bytes:
    """Read one (or more) byte(s) in the file at `offset`.

    This is mostly used to read instructions.

    Arguments:
        offset: File offset to read
        size: Number of bytes to read

    Returns:
        The bytes values
    """
    return self.read(offset, size)

read_int(offset, size, signed=False)

Read an integer from the binary.

Parameters:

Name Type Description Default
offset int

Integer file offset

required
size int

Integer size in bytes

required
Source code in bindings/python/quokka/executable.py
122
123
124
125
126
127
128
129
def read_int(self, offset: int, size: int, signed: bool = False) -> int:
    """Read an integer from the binary.

    Arguments:
        offset: Integer file offset
        size: Integer size in bytes"""
    en = {Endianness.BIG_ENDIAN: "big", Endianness.LITTLE_ENDIAN: "little"}[self.endianness]
    return int.from_bytes(self.read(offset, size), en, signed=signed) # type: ignore

read_string(offset, size=None)

Read a string in the file.

If the size is not given, Quokka will try to read the string until the first null byte. That works only for null-terminated strings.

If the string is null terminated, remove the trailing 0.

Parameters:

Name Type Description Default
offset int

String file offset

required
size int | None

String size if known.

None

Returns:

Type Description
str

The decoded string

Raises:

Type Description
ValueError

If the string is not found nor decoded.

Source code in bindings/python/quokka/executable.py
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
def read_string(self, offset: int, size: int|None = None) -> str:
    """Read a string in the file.

    If the size is not given, Quokka will try to read the string until the
    first null byte. That works only for null-terminated strings.

    If the string is null terminated, remove the trailing 0.

    Arguments:
        offset: String file offset
        size: String size if known.

    Returns:
        The decoded string

    Raises:
      ValueError: If the string is not found nor decoded.
    """

    if size is not None:
        try:
            string = self.read(offset, size).decode("utf-8")
        except UnicodeDecodeError as exc:
            raise ValueError("Unable to read or decode the string.") from exc

    else:
        try:
            null_byte = self.content.index(b"\x00", offset)
        except ValueError as exc:
            raise ValueError(
                "String is not null-terminated and size was not given"
            ) from exc

        string = self.content[offset:null_byte].decode("utf-8")

    # FIX: When returning a single character string, it does not end with a '\0'
    if len(string) > 1 and string.endswith("\x00"):
        return string[:-1]

    return string

read_struct(offset, struct)

Read a struct from the binary.

Parameters:

Name Type Description Default
offset int

Struct file offset

required
type

Struct type

required
Source code in bindings/python/quokka/executable.py
171
172
173
174
175
176
177
178
179
180
181
def read_struct(self, offset: int, struct: StructureType | UnionType) -> bytes:
    """Read a struct from the binary.

    Arguments:
        offset: Struct file offset
        type: Struct type"""
    if struct.is_variable_size():
        logging.warning("Cannot read a variable size struct")
        return b""
    # FEATURE: Read a really structure instance
    return self.read_bytes(offset, struct.size)

read_type_value(offset, type)

Read the data value based on its type.

Parameters:

Name Type Description Default
offset int

Data file offset

required
data_type

Data type

required

Returns:

Type Description
TypeValue

The data value

Raises:

Type Description
ValueError

If the type is not supported or the value cannot be read.

Source code in bindings/python/quokka/executable.py
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
def read_type_value(self, offset: int, type: TypeT) -> TypeValue:
    """Read the data value based on its type.

    Arguments:
        offset: Data file offset
        data_type: Data type

    Returns:
        The data value

    Raises:
        ValueError: If the type is not supported or the value cannot be read.
    """
    en = {Endianness.BIG_ENDIAN: ">", Endianness.LITTLE_ENDIAN: "<"}[self.endianness]

    try:
        if type.is_base_type:
            match type:
                case BaseType.FLOAT:
                    return struct.unpack(f"{en}f", self.read_bytes(offset, 4))[0]
                case BaseType.DOUBLE:
                    return struct.unpack(f"{en}d", self.read_bytes(offset, 8))[0]
                case _:
                    return self.read_int(offset, type.size)
        elif type.is_array:
            return self.read_bytes(offset, type.size)
        elif type.is_pointer:
            return self.read_int(offset, type.size)
        elif type.is_struct or type.is_union:
            assert isinstance(type, (StructureType, UnionType))
            return self.read_struct(offset, type)
        elif type.is_enum:
            assert isinstance(type, EnumType)
            return self.read_enum(offset, type)
        else:
            assert False, f"Unsupported type {type}"
    except Exception as exc:
        raise ValueError(f"Unable to read value of type {type} at offset {offset}") from exc