#!/usr/bin/python

# rpi-eeprom-config
# Utility for reading and writing the configuration file in the
# Raspberry Pi4 bootloader EEPROM image.

import argparse
import struct
import sys

IMAGE_SIZE = 512 * 1024

MAX_BOOTCONF_SIZE = 2024

# Each section starts with a magic number followed by a 32 bit offset to the
# next section (big-endian).
# The number, order and size of the sections depends on the bootloader version
# but the following mask can be used to test for section headers and skip
# unknown data.
MAGIC = 0x55aaf00f
MAGIC_MASK = 0xfffff00f
FILE_MAGIC = 0x55aaf11f # id for modifiable file, currently only bootconf.txt
FILE_HDR_LEN = 20
FILENAME_LEN = 12

class BootloaderImage(object):
    def __init__(self, filename, output):
        self._filename = filename
        self._bytes = bytearray(open(filename, 'rb').read())
        self._out = None
        if output is not None:
            self._out = open(output, 'wb')

        if len(self._bytes) != IMAGE_SIZE:
            raise Exception("%s: Expected size %d bytes actual size %d bytes" %
                            (filename, IMAGE_SIZE, len(self._bytes)))

    def find_config(self):
        offset = 0
        magic = 0
        while offset < IMAGE_SIZE:
            magic, length = struct.unpack_from('>LL', self._bytes, offset)
            if (magic & MAGIC_MASK) != MAGIC:
                raise Exception('EEPROM is corrupted')

            if magic == FILE_MAGIC: # Found a file
                name = self._bytes[offset + 8: offset + FILE_HDR_LEN]
                if name.decode('utf-8') == 'bootconf.txt':
                    return (offset, length)

            offset += 4 + length
            offset += 8 - (offset % 8) # Pad

        raise Exception('Bootloader config not found')

    def write(self, new_config):
        hdr_offset, length = self.find_config()
        new_config_bytes = open(new_config, 'rb').read()
        new_len = len(new_config_bytes) + FILENAME_LEN + 4
        if len(new_config_bytes) > MAX_BOOTCONF_SIZE:
            raise Exception("Config is too large (%d bytes). The maximum size is %d bytes."
                    % (len(new_config_bytes), MAX_BOOTCONF_SIZE))
        if hdr_offset + len(new_config_bytes) + FILE_HDR_LEN > IMAGE_SIZE:
            raise Exception('EEPROM image size exceeded')

        struct.pack_into('>L', self._bytes, hdr_offset + 4, new_len)
        struct.pack_into(("%ds" % len(new_config_bytes)), self._bytes, hdr_offset + 4 + FILE_HDR_LEN, new_config_bytes)

        # If the new config is smaller than the old config then set any old
        # data which is now unused to all ones (erase value)
        pad_start = hdr_offset + 4 + FILE_HDR_LEN + len(new_config_bytes)
        pad = 0
        while pad < (length - len(new_config_bytes)):
            struct.pack_into('B', self._bytes, pad_start + pad, 0xff)
            pad = pad + 1

        if self._out is not None:
            self._out.write(self._bytes)
            self._out.close()
        else:
            if hasattr(sys.stdout, 'buffer'):
                sys.stdout.buffer.write(self._bytes)
            else:
                sys.stdout.write(self._bytes)

    def read(self):
        hdr_offset, length = self.find_config()
        offset = hdr_offset + 4 + FILE_HDR_LEN
        config_bytes = self._bytes[offset:offset+length-FILENAME_LEN-4]
        if self._out is not None:
            self._out.write(config_bytes)
            self._out.close()
        else:
            if hasattr(sys.stdout, 'buffer'):
                sys.stdout.buffer.write(config_bytes)
            else:
                sys.stdout.write(config_bytes)

def main():
    parser = argparse.ArgumentParser('RPI EEPROM config tool')
    parser.add_argument('--config', help='Filename of new bootloader config')
    parser.add_argument('--out', help='Filename for the EEPROM image with updated config')
    parser.add_argument('eeprom', help='EEPROM filename (pieeprom.bin)')
    args = parser.parse_args()

    image = BootloaderImage(args.eeprom, args.out)
    if args.config is not None:
        image.write(args.config)
    else:
        image.read()

if __name__ == '__main__':
    main()
