Metadata-Version: 2.4
Name: pygbl
Version: 1.0.0
Summary: Parser for Silicon Labs GBL and EBL firmware images
Author-email: puddly <puddly3@gmail.com>
License-Expression: Apache-2.0
Project-URL: repository, https://github.com/zigpy/pygbl
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pyelftools
Provides-Extra: crypto
Requires-Dist: cryptography; extra == "crypto"
Provides-Extra: lz4
Requires-Dist: lz4; extra == "lz4"
Provides-Extra: all
Requires-Dist: pygbl[crypto,lz4]; extra == "all"
Provides-Extra: dev
Requires-Dist: pygbl[all]; extra == "dev"
Requires-Dist: uv>=0.11.14; extra == "dev"
Requires-Dist: ruff>=0.14.6; extra == "dev"
Requires-Dist: mypy>=2.1.0; extra == "dev"
Requires-Dist: codespell>=2.4.2; extra == "dev"
Requires-Dist: pytest>=9.0.1; extra == "dev"
Requires-Dist: prek>=0.3.11; extra == "dev"
Requires-Dist: pytest-timeout>=2.4.0; extra == "dev"
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
Requires-Dist: types-setuptools>=82.0.0.20260508; extra == "dev"
Dynamic: license-file

# pygbl

A parser for the EBL and GBL (v3 and v4) firmware image formats used by the
[Silicon Labs Gecko bootloader](https://www.silabs.com/documents/public/user-guides/ug489-gecko-bootloader-user-guide-gsdk-4.pdf).

Images are parsed into structured tags, and round-trip byte-for-byte: anything the
library reads it can write back unchanged, including the trailing data that some vendors
append after the end tag.

## Installation

```console
pip install pygbl
```

The base install is pure Python and covers parsing, serializing, building images from
ELFs, and LZMA. Signing, encryption and LZ4 need optional dependencies:

```console
pip install pygbl[crypto]  # signing and encryption
pip install pygbl[lz4]     # LZ4 compression
pip install pygbl[all]
```

Calling those features without their dependency raises `MissingDependencyError`.

## Usage

Parse an image and inspect its tags:

```python
import pathlib

from pygbl import GBL3ApplicationInfo, parse_firmware_image

data = pathlib.Path("ncp-uart-hw.gbl").read_bytes()
image = parse_firmware_image(data)

for tag in image.tags:
    print(tag)

print(image.get_first_tag(GBL3ApplicationInfo).version)
print(image.get_metadata())  # opaque bytes, the schema is vendor defined

assert image.serialize() == data
```

Modify an image. Tags are frozen dataclasses, so `dataclasses.replace` works, and
`regenerate_crc` fixes up the end tag afterwards:

```python
import dataclasses

from pygbl import GBL3End, GBL3Metadata

modified = dataclasses.replace(
    image,
    tags=[t for t in image.tags if not isinstance(t, GBL3End)]
    + [GBL3Metadata(metadata=b'{"fw_type": "zigbee_ncp"}')],
).regenerate_crc()
```

Compress, encrypt and sign, in the order the bootloader expects:

```python
from cryptography.hazmat.primitives.serialization import load_pem_private_key

from pygbl import GBL3Compression

private_key = load_pem_private_key(
    pathlib.Path("vendor_sign.key").read_bytes(), password=None
)
key = bytes.fromhex("7F8FE53979B31BC556FCB131AFF42414")

sealed = image.compress(GBL3Compression.LZMA).encrypt(key).sign(private_key)
pathlib.Path("signed.gbl").write_bytes(sealed.serialize(block_size=4))
```

And unwrap it again:

```python
assert sealed.verify_signature(private_key.public_key())
assert sealed.decrypt(key).decompress().serialize() == image.serialize()
```

## Building images from an ELF

Images can be built straight from a linked ELF, without `commander`. Program data comes
from the loadable segments (keyed by *physical* address, since initialized data is
stored in flash but linked at its RAM address) and the application info tag is read
from the SDK's `application_properties_t` struct:

```python
from pygbl import build_application_gbl3, build_bootloader_gbl3

with open("zigbee_ncp.out", "rb") as f:
    image = build_application_gbl3(f, metadata=b'{"fw_type": "zigbee_ncp"}')

with open("bootloader.out", "rb") as f:
    bootloader = build_bootloader_gbl3(f)
```

Both reproduce `commander gbl create` byte-for-byte. Bootloader payloads carry a CRC32
of themselves, which `build_bootloader_gbl3` appends for you.

## GBLv4

Series 3 parts use GBLv4, a different format that nests tags inside a signed manifest
and can bundle several updates in one file. `parse_firmware_image` recognizes it by its
magic and returns a `GBL4Image`, which round-trips byte-for-byte like the rest:

```python
from pygbl import GBL4Image, GBL4MemorySectionInfo, GBL4UpdateMemorySection

image = parse_firmware_image(pathlib.Path("light-simg301.gbl4").read_bytes())
assert isinstance(image, GBL4Image)
assert image.serialize() == data

# `get_tags` searches the whole tree, at any depth
for update in image.get_tags(GBL4UpdateMemorySection):
    print(f"{update.target_address:#010x} {update.plain_image_size} bytes")

for info in image.get_tags(GBL4MemorySectionInfo):
    print(info.compression_scheme, info.encryption_scheme, info.nonce.hex())
```

Reading and writing are supported; building a v4 image from an ELF is not, the ELF
helpers are GBLv3 only.

## EBL

EBL images, used by older EM3xx parts, work the same way:

```python
from pygbl import EBLEraseProgram, parse_firmware_image

image = parse_firmware_image(pathlib.Path("ncp-uart-sw.ebl").read_bytes())

for tag in image.get_tags(EBLEraseProgram):
    print(f"{tag.address:#010x} {len(tag.data)} bytes")
```

## Bootloader and application images

A GBL can contain a bootloader, an application, or both. Combined images can be split
apart and recombined, which is useful because some bootloaders cannot flash a combined
image in one pass:

```python
bootloader, application = combined.split_bootloader_app()
recombined = application.combine_bootloader_app(bootloader)
```
