Example #1
0
    def update_ucode_fit_entry (ifwi_bin, ucode_path):
        ifwi = IFWI_PARSER.parse_ifwi_binary (ifwi_bin)
        if not ifwi:
            print ("Not a valid ifwi image!")
            return -2

        # Get microcode
        ucode_comps = IFWI_PARSER.locate_components (ifwi, ucode_path)
        if len(ucode_comps) == 0:
            print ("Cannot find microcode component in ifwi image!" % path)
            return -3

        # Get partition from path
        bp = IFWI_PARSER.get_boot_partition_from_path (ucode_path)

        # Get fit entry
        path = 'IFWI/BIOS/TS%d/SG1A' % bp
        ifwi_comps = IFWI_PARSER.locate_components (ifwi, path)
        if len(ifwi_comps) == 0:
            path = 'IFWI/BIOS/SG1A' % bp
            ifwi_comps = IFWI_PARSER.locate_components (ifwi, path)
        if len(ifwi_comps) == 0:
            print ("Cannot find 'SG1A' in ifwi image!" % path)
            return -4

        img_base   = 0x100000000 - len(ifwi_bin)
        fit_addr   = c_uint32.from_buffer(ifwi_bin, ifwi_comps[0].offset + ifwi_comps[0].length + FIT_ENTRY.FIT_OFFSET)
        fit_offset = fit_addr.value - img_base
        fit_header = FIT_ENTRY.from_buffer(ifwi_bin, fit_offset)

        if fit_header.address != bytes_to_value (bytearray(FIT_ENTRY.FIT_SIGNATURE)):
            print ("Cannot find FIT table !" % path)
            return -4

        # Update Ucode entry address
        ucode_idx  = 0
        ucode_off  = ucode_comps[0].offset
        ucode_list = UCODE_PARSER.parse (ifwi_bin[ucode_off:])
        for fit_type in [0x01, 0x7f]:
            for idx in range(fit_header.size):
                fit_entry = FIT_ENTRY.from_buffer(ifwi_bin, fit_offset + (idx + 1) * 16)
                if fit_entry.type == fit_type:
                    if ucode_idx < len(ucode_list):
                        fit_entry.set_values(img_base + ucode_off, 0, 0x100, 0x1, 0)
                        ucode_off += len(ucode_list[ucode_idx])
                        ucode_idx += 1
                    else:
                        # more fit entry is available, clear this entry
                        fit_entry.type = 0x7f

        if ucode_idx != len(ucode_list):
            print ("Not all microcode can be listed in FIT table due to limited FIT entry number !")
            return -5

        # Update FIT checksum
        fit_header.checksum  = 0
        fit_sum = sum(ifwi_bin[fit_offset:fit_offset+fit_header.size*16])
        fit_header.checksum = (0 - fit_sum) & 0xff
        return 0
Example #2
0
 def find_ifwi_region (spi_descriptor, rgn_name):
     frba      = ((spi_descriptor.fl_map0 >> 16) & 0xFF) << 4
     fl_reg    = spi_descriptor.FLASH_REGIONS[rgn_name] + frba
     rgn_off   = c_uint32.from_buffer(spi_descriptor, fl_reg)
     rgn_base  = (rgn_off.value & 0x7FFF) << 12
     rgn_limit = ((rgn_off.value & 0x7FFF0000) >> 4) | 0xFFF
     if rgn_limit <= rgn_base:
         return None, None
     else:
         return (rgn_base, rgn_limit)
Example #3
0
 def FindIfwiRegion (self, SpiDescriptor, RgnName):
     Frba = ((SpiDescriptor.FlMap0 >> 16) & 0xFF) << 4
     FlReg = SpiDescriptor.FLASH_REGIONS[RgnName] + Frba
     RgnOff = c_uint32.from_buffer(SpiDescriptor, FlReg)
     RgnBase = (RgnOff.value & 0x7FFF) << 12
     RgnLimit = ((RgnOff.value & 0x7FFF0000) >> 4) | 0xFFF
     if RgnLimit <= RgnBase:
         return None
     else:
         return (RgnBase, RgnLimit)
Example #4
0
    def _check_is_macho_header(self, offset: StaticFilePointer) -> bool:
        """Check if the data located at a file offset represents a valid Mach-O header, based on the magic

        Args:
            offset: File offset to read magic from

        Returns:
            True if the byte content of the file at 'offset' contain the magic number for a Mach-O slice,
            False if the magic is anything else

        """
        magic = c_uint32.from_buffer(bytearray(self.get_bytes(offset, sizeof(c_uint32)))).value
        return magic in MachoParser._MACHO_MAGIC
Example #5
0
    def FindComponent (self, BiosBins, Name):
        # Find CFGDATA in SlimBoot image
        BiosSize = len(BiosBins)
        BiosBase = 0x100000000 - BiosSize

        FlashMapAddr = c_uint32.from_buffer(BiosBins, BiosSize - 8)
        FlashMapOff = FlashMapAddr.value - BiosBase
        FlashMapStr = FlashMap.from_buffer(BiosBins, FlashMapOff)

        CompList = []
        if FlashMapStr.sig == FlashMap.FLASH_MAP_SIGNATURE:
            Offset = BiosSize
            EntryNum = (
                FlashMapStr.length - sizeof(FlashMap)) // sizeof(FlashMapDesc)
            for idx in xrange(EntryNum):
                Desc = FlashMapDesc.from_buffer(
                    BiosBins,
                    FlashMapOff + sizeof(FlashMap) + idx * sizeof(FlashMapDesc))
                Offset -= Desc.size
                # print '%s @ offset 0x%06X size 0x%x' %  (Desc.sig, offset, Desc.size)
                if Name == Desc.sig:
                    CompList.append((Offset, Desc.size))

        return CompList
def handle_ts(bios_image, set_ts_val=0):

    # Since QEMU does not support flash top swap, this script will help
    # reassemble the image according to the top swap request
    inf = open(bios_image, "rb")
    bios_bins = bytearray(inf.read())
    inf.close()

    # Get flash map
    bios_size = len(bios_bins)
    bios_base = 0x100000000 - bios_size

    flash_map_addr = c_uint32.from_buffer(bios_bins, bios_size - 8)
    flash_map_off = flash_map_addr.value - bios_base

    flash_map = FlashMap.from_buffer(bios_bins, flash_map_off)
    is_backup = 1 if flash_map.attributes & flash_map.FLASH_MAP_ATTRIBUTES[
        'BACKUP_REGION'] else 0

    top_swap_size = 0
    redundant_size = 0
    entry_num = (flash_map.length - sizeof(FlashMap)) // sizeof(FlashMapDesc)
    for idx in range(entry_num):
        desc = FlashMapDesc.from_buffer(
            bios_bins,
            flash_map_off + sizeof(FlashMap) + idx * sizeof(FlashMapDesc))
        if (desc.flags & FlashMap.FLASH_MAP_DESC_FLAGS['TOP_SWAP']
            ) and not (desc.flags & FlashMap.FLASH_MAP_DESC_FLAGS['BACKUP']):
            top_swap_size += desc.size
        if (desc.flags & FlashMap.FLASH_MAP_DESC_FLAGS['REDUNDANT']
            ) and not (desc.flags & FlashMap.FLASH_MAP_DESC_FLAGS['BACKUP']):
            redundant_size += desc.size

    top_a = None
    top_b = None

    curr = top_swap_size
    tmp = bios_bins[-curr:]

    # Get the FWU flag
    fwu_flg = tmp[-10]
    if fwu_flg != 0x90:
        tmp[-10] = 0x90

    # Get the top swap request from image
    ts_req = tmp[-11]
    if ts_req != 0x90:
        tmp[-11] = 0x90

    print("---------------------------------------------")
    print("State: %02X %02X" % (ts_req, fwu_flg))

    if ts_req != 0x90:
        print("Automatically change TS !")
    else:
        print("Manually change TS !")
        inp = set_ts_val
        if inp == 0:
            ts_req = 0x00
        elif inp == 1:
            ts_req = 0x80
        else:
            ts_req = 0x90

    print("Booting from BP%d" % is_backup)
    if is_backup:
        top_b = tmp
    else:
        top_a = tmp

    curr += top_swap_size
    tmp = bios_bins[-curr:-curr + top_swap_size]

    if is_backup:
        top_a = tmp
    else:
        top_b = tmp

    top_n = '\xff' * top_swap_size
    redundant_n = '\xff' * redundant_size

    curr += redundant_size
    redundant_a = bios_bins[-curr:-curr + redundant_size]

    curr += redundant_size
    redundant_b = bios_bins[-curr:-curr + redundant_size]

    non_redundant = bios_bins[:bios_size - curr]

    if ts_req == 0x00:
        ts = 0
        tv = '0'
    elif ts_req == 0x80:
        ts = 1
        tv = '1'
    else:
        # do not change anything
        ts = 0x80
        tv = 'N'

    print("TS Current:    (%d)" % (is_backup))
    print("TS  Reqest: %02X (%s)" % (ts_req, tv))
    print("FWU  Flags: %02X (%d)" % (fwu_flg, 0 if fwu_flg == 0x90 else 1))

    if ts == 0x80:
        bios_dat = bios_bins
        print("No change")
    else:
        if ts & 1 == 0:
            # Boot from partition 0
            bios_dat = non_redundant + redundant_b + redundant_a + top_b + top_a
            print("Activate Partition A")
        else:
            # Boot from partition 1
            bios_dat = non_redundant + redundant_b + redundant_a + top_a + top_b
            print("Activate Partition B")

    inf = open(bios_image, "wb")
    inf.write(bios_dat)
    inf.close()

    return fwu_flg
Example #7
0
def int_from_buffer(mmap_area, pos):
    return c_uint32.from_buffer(mmap_area, pos)  #@UndefinedVariable
Example #8
0
 def file_magic(self) -> int:
     """Read file magic
     """
     return c_uint32.from_buffer(bytearray(self.get_bytes(StaticFilePointer(0), sizeof(c_uint32)))).value
Example #9
0
def cmd_create(args):
    """Create an ias-image"""
    print('Creating ias-image with %d files' % len(args.file))

    # dictionary with image types names
    image_types_names = {IasHeader.TYPE_UNKNOWN: 'Unspecified', \
                         IasHeader.TYPE_KERNEL_CMDLINE: 'Linux command line', \
                         IasHeader.TYPE_KERNEL_BZIMAGE: 'Linux kernel (bzImage)', \
                         IasHeader.TYPE_MULTIFILE_BOOT: 'Multi-file boot image', \
                         IasHeader.TYPE_ELF_MULTI_BOOT: 'Stand-alone ELF multi-boot', \
                         IasHeader.TYPE_UPDATE_PACKAGE: 'Update Package Image with extra header', \
                         IasHeader.TYPE_ABL_CONFIG: 'ABL Configuration', \
                         IasHeader.TYPE_MRC_TRAINING_PARAM: 'MRC training parameter set', \
                         IasHeader.TYPE_IFWI_UPDATE_PACKAGE: 'IFWI update package', \
                         IasHeader.TYPE_PDR_UPDATE_PACKAGE: 'PDR update package', \
                         IasHeader.TYPE_FW_PACKAGE: 'Firmware package', \
                         IasHeader.TYPE_PREOS_CHECKER: 'Pre-OS checker'}

    # list of all multiple files image types
    multi_files_image_type = [IasHeader.TYPE_UNKNOWN, \
                              IasHeader.TYPE_MULTIFILE_BOOT, \
                              IasHeader.TYPE_ELF_MULTI_BOOT, \
                              IasHeader.TYPE_FW_PACKAGE]

    # process command line parameters
    verbose = args.verbose
    if args.imagetype != None:
        try:
            image_type = int(args.imagetype, 0) & 0xF0000
        except ValueError:
            print("Error: No digits were found")
            return 1
        if image_type not in image_types_names:
            print("Error: Not supported type image")
            return 1
        public_key_present = int(args.imagetype, 0) & IasHeader.PUBKEY_PRESENT
        signature_present = int(args.imagetype, 0) & IasHeader.SIGNATURE_PRESENT
    else:
        image_type = int(IasHeader.TYPE_UNKNOWN)
        public_key_present = 0
        signature_present = 0
    page_aligned_num = 0

    print('Detected image type is (0x%x) - %s' % (image_type, image_types_names[image_type]))

    # set values of alignment
    if args.page_aligned is None:
        if verbose > 0:
            print("Files in image will not be page aligned")
    elif int(args.page_aligned) >= 0:
        page_aligned_num = int(args.page_aligned)
        # correct to default value if necessary
        if (page_aligned_num == 0) and (image_type == IasHeader.TYPE_MULTIFILE_BOOT):
            page_aligned_num = 5
            if verbose > 0:
                print('Creation of Multi-file boot image, default alignment from %d file' % page_aligned_num)
        elif (page_aligned_num == 0) and (image_type == IasHeader.TYPE_ELF_MULTI_BOOT):
            page_aligned_num = 4
            if verbose > 0:
                print('Creation of Stand-alone ELF multi-boot image, default alignment from %d file' % page_aligned_num)
        elif (page_aligned_num == 0) and (image_type == IasHeader.TYPE_FW_PACKAGE):
            page_aligned_num = 2
            if verbose > 0:
                print('Creation of Firmware Package Image, default alignment from %d file' % page_aligned_num)
        elif (page_aligned_num >= 0) and (multi_files_image_type.count(image_type) == 0):
            sys.stderr.write(
                'Page alignment not supported for image type 0x%x (%s)\n' % (image_type, image_types_names[image_type]))
            return 1
        elif (page_aligned_num == 0) and (image_type == IasHeader.TYPE_UNKNOWN):
            page_aligned_num = 2  # default value for Unknown image

    if (verbose > 0) and (args.page_aligned != None):
        print('Page alignment for image from file %d detected' % page_aligned_num)

    # Read file data
    files = []
    for fpath in args.file:
        try:
            with open(fpath, 'rb') as input_file:
                sys.stdout.write('  %s ' % fpath)  # file path #
                files.append(bytearray(input_file.read()))
        except IOError:
            print('Error: No such file or directory: %s' % fpath)
            return 1
        print("(%s)" % human_size(len(files[-1])))  # file size #

    # check if there is enough files for page alignment, otherwise error
    if page_aligned_num > len(files):
        sys.stderr.write('Error. Page alignment number %d is higher than a number of input files %d\n' % (
            page_aligned_num, len(files)))
        return 1

    # Creating of Multi-file Boot image
    # Dummy files will be added if page alignment to 4KiB required
    if (image_type == IasHeader.TYPE_MULTIFILE_BOOT) or ((image_type == IasHeader.TYPE_UNKNOWN) and (len(files) > 1)):
        if len(files) < 1:
            print('Error: Please supply at least one input files')
            return 1

        # find number of required dummy files
        if args.page_aligned is None:
            dummy_files = 0
        else:
            dummy_files = len(files) - page_aligned_num + 1

        if verbose > 2:
            print('%d dummy files will be added to page align the image' % dummy_files)

        # set initial file offset (payload data offset in image)
        # Type specific header length   = 4 bytes (c_uint32) * number of files (input files + dummy files)
        file_offset = sizeof(IasHeader) + sizeof(c_uint32) * len(files) + sizeof(c_uint32) * dummy_files

        temp_len = 0
        while temp_len < len(files):
            if ((temp_len + 1) >= page_aligned_num) and (page_aligned_num != 0):
                # calculate size of dummy file
                dummy_size = align_up(file_offset, 4 * KB) - file_offset  #
                if dummy_size >= 0:
                    if verbose > 1:
                        print('Adding 0x%x (%d) bytes size dummy file' % (dummy_size, dummy_size))
                    files.insert(temp_len, bytearray(dummy_size))  # create array with dummy data
                    temp_len += 1  # set iteration to skip this file
                    file_offset += dummy_size
                # align up to 4B
                file_offset += align_up(len(files[temp_len]), 4)
                if verbose > 1:
                    print('Adding file of size: %d (0x%x)' % (len(files[temp_len]), len(files[temp_len])))
            else:  # align up to 4B only
                padding = align_up(len(files[temp_len]), 4) - len(files[temp_len])
                if verbose > 2:
                    print(' Adding 0x%x (%d) pad bytes to align file to 0x4' % (padding, padding))
                file_offset += padding
                file_offset += len(files[temp_len])

            temp_len += 1  # set to next file
            if verbose > 2:
                print('File offset is %d (0x%x)' % (file_offset, file_offset))

    # Creating of:
    # - Firmware-package image
    # - Elf-multiboot image
    # Command line files will be extended with 0x0 at the end if page alignment to 4KiB required
    elif (image_type == IasHeader.TYPE_ELF_MULTI_BOOT) or (image_type == IasHeader.TYPE_FW_PACKAGE):
        # initialize file offset (payload data start) in image
        file_offset = sizeof(IasHeader) + sizeof(c_uint32) * len(files)

        for temp_len in range(len(files)):
            # check if processing cmdline file
            if ((temp_len + 1) >= (page_aligned_num - 1)) and (((temp_len + 1) % 2) == 1) and (page_aligned_num != 0):
                # calculate padding
                padding = align_up(file_offset + len(files[temp_len]), 4 * KB) - (file_offset + len(files[temp_len]))
                if padding > 0:  # align up to 4KiB
                    if verbose > 1:
                        print('Adding 0x%x (%d) pad bytes to align cmdline' % (padding, padding))
                    files[temp_len] += bytearray(padding)
                    # add padding 0x0 bytes to file (cmdLine)
                file_offset += len(files[temp_len])
            else:  # align up to 4B only
                padding = align_up(len(files[temp_len]), 4) - len(files[temp_len])
                if verbose > 2:
                    print('Adding 0x%x (%d) pad bytes to align binary to 0x4' % (padding, padding))
                file_offset += padding
                file_offset += len(files[temp_len])

            if verbose > 2:
                print('File offset is %d (0x%x)' % (file_offset, file_offset))

    # single-file images
    # Only alignment to 4 bytes for file will be added
    else:
        if len(files) > 1:
            sys.stderr.write('Error: Please supply only one input file\n')
            return 1

        file_offset = sizeof(IasHeader)
        padding = align_up(len(files[0]), 4) - len(files[0])
        if verbose > 2:
            print('Note: Adding 0x%x (%d) pad bytes to align file to 0x4' % (padding, padding))
        file_offset += padding
        file_offset += len(files[0])

    # set image size
    image_size = file_offset
    image_size += sizeof(c_uint32)  # Checksum at the end of image payload

    # declare array for all files
    data = bytearray(image_size)
    ptr = 0

    # Create header
    hdr = IasHeader.from_buffer(data, ptr)
    hdr.magic_pattern = struct.unpack(">I", IasHeader.MAGIC)[0]  # ">I" big endian 4 bytes (integer)
    if args.imagetype is None:
        hdr.image_type = image_type
    else:
        hdr.image_type = int(args.imagetype, 0)
    if args.devkey:
        if (not public_key_present) | (not signature_present):
            print('WARNING: No public key or signature flag in image type, adding both')
        hdr.image_type |= IasHeader.SIGNATURE_PRESENT
        hdr.image_type |= IasHeader.PUBKEY_PRESENT
    hdr.version = 0
    if len(files) >= 1:
        hdr.data_length = file_offset - sizeof(IasHeader) - len(files) * sizeof(c_uint32)
    else:
        hdr.data_length = file_offset - sizeof(IasHeader)
    hdr.data_offset = 0
    hdr.uncompressed_len = hdr.data_length
    hdr.header_crc = 0
    ptr += sizeof(IasHeader)

    # Create extended header (for multiple files images, types 3,4,10) # in Type Specific Header field
    if len(files) >= 1:
        ehdr_start = ptr
        ehdr_limit = ehdr_start + sizeof(c_uint32) * len(files)
        ehdr = (c_uint32 * len(files)).from_buffer(data, ehdr_start)
        for i in range(len(files)):
            ehdr[i] = len(files[i])
            print('File %d size %d bytes' % (i + 1, ehdr[i]))
        ptr = ehdr_limit

    hdr.data_offset = ptr
    hdr.header_crc = crc32c_buf(data[0:24])

    # Add file data
    for item in range(len(files)):
        f_start = ptr
        f_limit = f_start + len(files[item])
        if verbose > 1:
            print('Adding file %d @ [0x%08x-0x%08x]' % (item + 1, f_start, f_limit))
        data[f_start:f_limit] = files[item]
        ptr = align_up(f_limit, 4)

    # Add payload checksum
    crc_start = ptr
    crc_limit = crc_start + sizeof(c_uint32)
    crc = c_uint32.from_buffer(data, crc_start)
    sys.stdout.write('Calculating Checksum... ')
    crc.value = crc32c_buf(data[sizeof(hdr):hdr.data_offset + hdr.data_length])
    print('Ok')
    ptr = crc_limit

    # delete all the views that will prevent resizing the data buffer when
    # signing
    del hdr
    del ehdr
    del crc

    if args.devkey:
        sys.stdout.write('Signing... ')
        try:
            with open(args.devkey, 'rb') as rsa_key_file:
                key = serialization.load_pem_private_key(
                        rsa_key_file.read(),
                        password=None,
                        backend=default_backend()
                    )
        except IOError:
            print('Error: No such file or directory: %s' % args.devkey)
            return 1
        # Calculate a PKCS#1 v1.5 signature
        signature = key.sign(bytes(data),  crypto_padding.PKCS1v15(), hashes.SHA256())
        # Extract public key from loaded key
        puk = key.public_key()
        puk_num = puk.public_numbers()

        mod_buf = pack_num(puk_num.n, RSA_KEYMOD_SIZE)
        exp_buf = pack_num(puk_num.e, RSA_KEYEXP_SIZE)
        data += bytearray([0xff] * (align_up(ptr, 256) - ptr))
        data += signature
        data += reverse_bytearray(mod_buf) + exp_buf
        print('Ok')

    # Write file out
    sys.stdout.write('Writing... ')
    with open(args.output, 'wb') as output_file:
        output_file.write(data)
    print('Ok')

    return 0
Example #10
0
class Inotify:
    """Base class for `TreeWatcher`. Interfaces with inotify(7).

    While `TreeWatcher` provides functionality for watching directories
    recursively, this is suitable for watching a file (or files).

    Attributes:
        exclusive_handlers (dict): maps `INFlags` to sets of `Inotify.EventHandler`s.
            Handler is executed iff event.mask == handler mask.
        inclusive_handlers (dict): maps `INFlags` to sets of `Inotify.EventHandler`s.
            Handler will be executed if a bitwise AND of the event.mask with the handler
            mask is non-zero.
        inotify_fd (int): file descriptor returned by call to `inotify_init1`.
        watch_flags (INFlags): flags to be passed to `inotify_add_watch`.
        watch_fds (dict): a dict mapping watch descriptors to their associated filenames.
        files (set): a set of filenames currently being watched.
        n_buffers (int): number of per instance read buffers.
        read_buffers (list): per instance read buffers.
        max_read (int): maximum bytes we can read.
        buf_size (int): in bytes.
        LEN_OFFSET (int): we need to read the length of the name before unpacking the
            bytes to the struct format. See the underlying struct inotify_event.
    """

    LEN_OFFSET = sizeof(c_int) + sizeof(c_uint32) * 2
    EventHandler = Callable[["Inotify", "InotifyEvent"], None]

    def __init__(
        self,
        *files: str,
        blocking: bool = True,
        watch_flags: INFlags = INFlags.NO_FLAGS,
        n_buffers: int = 1,
        buf_size: int = 1024,
    ):
        init_flags = INFlags.NO_FLAGS if blocking else INFlags.NONBLOCK
        self.inotify_fd = inotify_init1(init_flags)
        if self.inotify_fd < 0:
            raise OSError(os.strerror(get_errno()))
        self.n_buffers = n_buffers
        self.read_buffers = [bytearray(buf_size) for _ in range(n_buffers)]
        self.max_read = n_buffers * buf_size
        self.buf_size = buf_size
        self.exclusive_handlers: Dict[INFlags, Set[Inotify.EventHandler]] = {}
        self.inclusive_handlers: Dict[INFlags, Set[Inotify.EventHandler]] = {}
        self.watch_flags = watch_flags
        self.watch_fds: Dict[int, str] = {}
        self.files: Set[str] = set()
        for fname in files:
            self._add_watch(os.path.abspath(fname))

    def teardown(self) -> None:
        for fd in self.watch_fds:
            self._rm_watch(fd)
        os.close(self.inotify_fd)

    def register_handler(self,
                         event_mask: INFlags,
                         handler: Inotify.EventHandler,
                         exclusive=True):
        """Register a handler for matching events.

        Args:
            event_mask (INFlags): event mask to match.
            handler (Inotify.EventHandler): handler to call on matching event.
            exclusive (bool): whether to register it as an exclusive handler (otherwise,
                it will be inclusive).
        """
        container = self.exclusive_handlers if exclusive else self.inclusive_handlers
        if event_mask in container:
            container[event_mask].add(handler)
        else:
            container[event_mask] = {handler}

    def _add_watch(self,
                   fname: str,
                   add_flags: INFlags = INFlags.MASK_CREATE) -> None:
        """Add an inotify watch for given file and update instance helper fields.

        Args:
            fname (string): file to watch.
            add_flags (INFlags): flags to pass to `inotify_add_watch`.

        Raises:
            OSError: `inotify_add_watch` returned -1 and set errno.
        """
        if not os.path.exists(fname):
            raise FileNotFoundError(fname)
        watch_fd = inotify_add_watch(
            c_int(self.inotify_fd),
            c_char_p(fname.encode("utf-8")),
            c_uint32(self.watch_flags | add_flags),
        )
        if watch_fd < 0:
            err = os.strerror(get_errno())
            raise OSError(err)
        self.files.add(fname)
        self.watch_fds[watch_fd] = fname

    def _rm_watch(self, fd: int) -> None:
        if inotify_rm_watch(c_int(self.inotify_fd), c_int(fd)) < 0:
            print(
                f"Inotify: got error removing watch fd {fd} ({self.watch_fds[fd]}):",
                file=sys.stderr,
            )
            print(os.strerror(get_errno()), file=sys.stderr)

    def get_event_abs_path(self, event: InotifyEvent) -> str:
        return f"{self.watch_fds[event.wd]}/{event.name}"

    def _handle_event(self, event: InotifyEvent) -> None:
        """Called for every event read from the inotify fd.

        To match events to exclusive handlers:
        - lookup event.mask in self.exclusive_handlers; and
        - execute every handler in the associated set.

        To match events to inclusive handlers:
        - iterate over the keys of self.inclusive_handlers;
        - bitwise AND each key with the event.handler; and
        - execute every handler in the associated set, iff the result of
        the AND is non-zero.

        Args:
            event (InotifyEvent): event read from the inotify fd.
        """
        for handler in self.exclusive_handlers.get(INFlags(event.mask), []):
            handler(self, event)

        if self.inclusive_handlers:
            for _, handlers in filter(lambda x: x[0] & event.mask,
                                      self.inclusive_handlers.items()):
                for handler in handlers:
                    handler(self, event)

    @staticmethod
    def get_event_struct_format(name_len: int) -> str:
        """Given an event with name of length (name_len), return
        the correct format string to pass to struct.unpack.

        Args:
            name_len (int): length of event name, taken from
            struct inotify_event.len.

        Returns:
            the struct format string.
        """
        return f"iIII{name_len}s"

    def read(self) -> int:
        """Read from the inotify fd into buffers, unpack bytes to
        InotifyEvent instances, and call the event handler.

        Returns: number of bytes read.

        Raises:
            BufferError: bytes_read was equal to the total combined
                buffer size.
        """
        if (bytes_read := os.readv(self.inotify_fd,
                                   self.read_buffers)) == self.max_read:
            raise BufferError("Inotify.read exceeded allocated buffers")

        if bytes_read < self.buf_size:
            buf = self.read_buffers[0]
        else:
            buf = reduce(add,
                         self.read_buffers[:ceil(bytes_read / self.buf_size)])
        offset = 0
        while offset < bytes_read:
            name_len = c_uint32.from_buffer(buf, offset + self.LEN_OFFSET)
            fmt = self.get_event_struct_format(name_len.value)
            obj_size = calcsize(fmt)
            self._handle_event(
                InotifyEvent.from_struct(
                    unpack(fmt, buf[offset:offset + obj_size])))
            offset += obj_size
        return bytes_read