Repository: malwarefrank/dnfile (language: python)

Issue:
research: are stream names guaranteed to be ASCII?
As noted in https://github.com/malwarefrank/dnfile/pull/17#issuecomment-992040726
are stream names guaranteed to be ASCII? What does the spec say? What do implementations do?


Discussion / hints:

Fix patch:
diff --git a/.github/mypy/mypy.ini b/.github/mypy/mypy.ini
new file mode 100644
index 0000000..ca06c89
--- /dev/null
+++ b/.github/mypy/mypy.ini
@@ -0,0 +1,4 @@
+[mypy]
+
+[mypy-pefile.*]
+ignore_missing_imports = True
\ No newline at end of file
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 861500a..1936265 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -18,6 +18,8 @@ jobs:
       run: isort --profile black --length-sort --line-width 120 -c .
     - name: Lint with pycodestyle
       run: pycodestyle --show-source src/dnfile examples tests
+    - name: Check types with mypy
+      run: mypy --config-file .github/mypy/mypy.ini src/dnfile tests/
 
   test:
     runs-on: ubuntu-20.04
diff --git a/examples/dndump.py b/examples/dndump.py
index 3655de2..b0a2dd6 100644
--- a/examples/dndump.py
+++ b/examples/dndump.py
@@ -114,21 +114,21 @@ def get_field_name(row, field):
     # map from something like `TypeName_StringIndex` to `TypeName`.
     # the former is the raw property name,
     # while the latter is the property we can access on the object.
-    if field in (row._struct_strings or ()):
+    if field in getattr(row, "_struct_strings", ()):
         fieldname = row._struct_strings[field]
-    elif field in (row._struct_guids or ()):
+    elif field in getattr(row, "_struct_guids", ()):
         fieldname = row._struct_guids[field]
-    elif field in (row._struct_blobs or ()):
+    elif field in getattr(row, "_struct_blobs", ()):
         fieldname = row._struct_blobs[field]
-    elif field in (row._struct_asis or ()):
+    elif field in getattr(row, "_struct_asis", ()):
         fieldname = row._struct_asis[field]
-    elif field in (row._struct_codedindexes or ()):
+    elif field in getattr(row, "_struct_codedindexes", ()):
         fieldname = row._struct_codedindexes[field][0]
-    elif field in (row._struct_indexes or ()):
+    elif field in getattr(row, "_struct_indexes", ()):
         fieldname = row._struct_indexes[field][0]
-    elif field in (row._struct_flags or ()):
+    elif field in getattr(row, "_struct_flags", ()):
         fieldname = row._struct_flags[field][0]
-    elif field in (row._struct_lists or ()):
+    elif field in getattr(row, "_struct_lists", ()):
         fieldname = row._struct_lists[field][0]
     else:
         # its not a special property,
@@ -149,7 +149,10 @@ def render_pe(ostream: Formatter, dn):
 
     with indenting(ostream):
         for stream in dn.net.metadata.streams_list:
-            ostream.writeln(stream.struct.Name.decode("utf-8") + ":")
+            try:
+                ostream.writeln(stream.struct.Name.decode("utf-8") + ":")
+            except UnicodeDecodeError:
+                ostream.writeln("(invalid){!r}".format(stream.struct.Name))
 
             with indenting(ostream):
                 render_pefile_struct(ostream, stream.struct)
@@ -188,7 +191,11 @@ def render_pe(ostream: Formatter, dn):
                                 value = "<TODO: not implemented in dnfile>"
                             else:
                                 if isinstance(v, dnfile.base.MDTableIndex):
-                                    name = None if not hasattr(v, "table") else v.table.name
+                                    if not hasattr(v, "table") or v.table is None:
+                                        logger.warning("reference has no table: %s", v)
+                                        name = "(missing)"
+                                    else:
+                                        name = v.table.name
                                     value = "ref table %s[%d]" % (name, v.row_index)
                                 elif isinstance(v, list):
                                     # will do this in a second pass
@@ -232,7 +239,12 @@ def render_pe(ostream: Formatter, dn):
                                 with indenting(ostream):
                                     for vv in v:
                                         if isinstance(vv, dnfile.base.MDTableIndex):
-                                            name = None if not hasattr(vv, "table") else vv.table.name
+                                            if not hasattr(vv, "table") or vv.table is None:
+                                                logger.warning("reference has no table: %s", vv)
+                                                name = "(missing)"
+                                            else:
+                                                name = vv.table.name
+
                                             ostream.writeln("ref table %s[%d]" % (name, vv.row_index))
                                         else:
                                             # at the moment, only MDTableIndexRefs are placed into lists.
diff --git a/examples/dnstrings.py b/examples/dnstrings.py
index 9770fcc..90f4d95 100755
--- a/examples/dnstrings.py
+++ b/examples/dnstrings.py
@@ -24,7 +24,11 @@ def show_strings(fname):
         # while there is still data in the stream
         while offset < size:
             # read the raw string bytes, and provide number of bytes read (includes encoded length)
-            buf, readlen = us.get_with_size(offset)
+            ret = us.get_with_size(offset)
+            if ret is None:
+                break
+
+            buf, readlen = ret
             # convert to a UserString object
             s = dnfile.stream.UserString(buf)
             # display the decoded string
diff --git a/examples/typeref-list.py b/examples/typeref-list.py
index 9b02c6c..431b210 100755
--- a/examples/typeref-list.py
+++ b/examples/typeref-list.py
@@ -13,8 +13,7 @@
         # if empty table (possible error with file), skip file
         continue
     # for each entry in the TypeRef table
-    for row in tr.rows:
-        row: dnfile.mdtable.TypeRefRow
+    for row in tr:
         # if the ResolutionScope includes a reference to another table
         if row.ResolutionScope and row.ResolutionScope.table:
             # make note of the table name
diff --git a/setup.py b/setup.py
index cf55d91..76a9a7b 100644
--- a/setup.py
+++ b/setup.py
@@ -20,6 +20,7 @@
     "pytest>=3",
     "isort==5.2.0",
     "pycodestyle==2.8.0",
+    "mypy==0.910",
 ]
 
 setup(
diff --git a/src/dnfile/__init__.py b/src/dnfile/__init__.py
index 56a8dba..5764687 100644
--- a/src/dnfile/__init__.py
+++ b/src/dnfile/__init__.py
@@ -18,13 +18,15 @@
 import copy as _copymod
 import codecs
 import struct as _struct
-from typing import Dict, List
+import logging
+from typing import Dict, List, Optional
 
 from pefile import PE as _PE
 from pefile import DIRECTORY_ENTRY, MAX_SYMBOL_EXPORT_COUNT, Dump, Structure, DataContainer, PEFormatError
 
-from . import enums, errors, stream
+from . import base, enums, errors, stream
 
+logger = logging.getLogger(__name__)
 CLR_METADATA_SIGNATURE = 0x424A5342
 
 
@@ -85,8 +87,8 @@ def dump_info(self, dump=None, encoding="utf-8"):
                 hasattr(self.net.metadata, "streams_list")
                 and self.net.metadata.streams_list
             ):
-                for s in self.net.metadata.streams_list:
-                    dump.add_lines(s.struct.dump(), indent=4)
+                for stream_ in self.net.metadata.streams_list:
+                    dump.add_lines(stream_.struct.dump(), indent=4)
                     dump.add_newline()
 
         # Metadata Tables
@@ -141,6 +143,17 @@ def parse_data_directories(
             ("IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR", self.parse_clr_structure),
         )
 
+        if self.__data__ is None:
+            logger.warning("not a .NET module: failed to read data")
+            self.net = None
+            return
+
+        opt_header = getattr(self, "OPTIONAL_HEADER", None)
+        if opt_header is None:
+            logger.warning("not a .NET module: no optional header")
+            self.net = None
+            return
+
         if directories is not None:
             if not isinstance(directories, (tuple, list)):
                 directories = [directories]
@@ -148,7 +161,7 @@ def parse_data_directories(
         for entry in directory_parsing:
             try:
                 directory_index = DIRECTORY_ENTRY[entry[0]]
-                dir_entry = self.OPTIONAL_HEADER.DATA_DIRECTORY[directory_index]
+                dir_entry = opt_header.DATA_DIRECTORY[directory_index]
             except IndexError:
                 break
 
@@ -178,7 +191,7 @@ def parse_data_directories(
         if not hasattr(self, attr_name):
             dir_entry_size = Structure(self.__IMAGE_DATA_DIRECTORY_format__).sizeof()
             dd_offset = (
-                self.OPTIONAL_HEADER.get_file_offset() + self.OPTIONAL_HEADER.sizeof()
+                opt_header.get_file_offset() + opt_header.sizeof()
             )
             clr_entry_offset = dd_offset + (
                 DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR"] * dir_entry_size
@@ -187,32 +200,37 @@ def parse_data_directories(
             dir_entry = self.__unpack_data__(
                 self.__IMAGE_DATA_DIRECTORY_format__, data, file_offset=clr_entry_offset
             )
+
             # if COM entry appears valid
-            if dir_entry.VirtualAddress:
+            if dir_entry is not None and dir_entry.VirtualAddress:
                 # try to parse the .NET CLR directory
                 value = self.parse_clr_structure(
                     dir_entry.VirtualAddress, dir_entry.Size
                 )
-                # if parsing was successful
                 if value:
-                    # set attribute
                     setattr(self, attr_name, value)
                     # create shortcut for .NET/CLR data
                     self.net = value
+                else:
+                    self.net = None
 
-    def parse_clr_structure(self, rva, size):
-        return ClrData(self, rva, size)
+    def parse_clr_structure(self, rva, size) -> Optional["ClrData"]:
+        try:
+            return ClrData(self, rva, size)
+        except errors.dnFormatError as e:
+            logger.warning("failed to parse CLR data: %s",  e)
+            return None
 
 
 class ClrMetaDataStruct(Structure):
-    Signature: int = None
-    MajorVersion: int = None
-    MinorVersion: int = None
-    Reserved: int = None
-    VersionLength: int = None
-    Version: int = None
-    Flags: int = None
-    NumberOfStreams: int = None
+    Signature: int
+    MajorVersion: int
+    MinorVersion: int
+    Reserved: int
+    VersionLength: int
+    Version: int
+    Flags: int
+    NumberOfStreams: int
 
 
 class ClrMetaData(DataContainer):
@@ -225,7 +243,7 @@ class ClrMetaData(DataContainer):
 
     rva: int
     struct: ClrMetaDataStruct
-    streams: Dict[int, base.ClrStream]
+    streams: Dict[bytes, base.ClrStream]
     streams_list: List[base.ClrStream]
 
     _format = (
@@ -327,8 +345,9 @@ def __init__(self, pe: dnPE, rva: int, size: int):
                 try:
                     s.parse(self.streams_list)
                 except (errors.dnFormatError, PEFormatError) as e:
-                    pe.add_warning("Unable to parse stream {}".format(s.struct.Name))
+                    pe.add_warning("Unable to parse stream {!r}".format(s.struct.Name))
                     pe.add_warning(str(e))
+                    logger.warning("unable to parse stream: %s: %s", s.struct.Name, e)
 
     def parse_stream_table(self, pe: dnPE, streams_table_rva):
         streams_list = list()
@@ -338,20 +357,23 @@ def parse_stream_table(self, pe: dnPE, streams_table_rva):
         for i in range(self.struct.NumberOfStreams):
             stream = ClrStreamFactory.createStream(pe, stream_entry_rva, self.rva)
             if not stream:
-                # error
-                pe.add_warning("Invalid .NET stream {}".format(i + 1))
+                logger.warning("Invalid .NET stream: {}".format(i + 1))
+                pe.add_warning("Invalid .NET stream: {}".format(i + 1))
                 # assume this throws off further parsing, so stop
                 break
+
             streams_list.append(stream)
-            # if a stream with this name already exists
-            if stream.struct.Name in streams_dict:
-                # warning
-                pe.add_warning(
-                    "Duplicate .NET stream name '{}'".format(stream.struct.Name)
-                )
-            else:
-                # otherwise add it to the associative array
-                streams_dict[stream.struct.Name] = stream
+            name = stream.struct.Name
+            if name in streams_dict:
+                # if a stream with this name already exists.
+                # this is not fatal, just unusual.
+                pe.add_warning("Duplicate .NET stream name '{!r}'".format(name))
+                logger.warning("Duplicate .NET stream name: %s", name)
+
+            # dotnet uses the last encountered stream with a given name,
+            # see: https://github.com/malwarefrank/dnfile/issues/19#issuecomment-992754448
+            # and test_invalid_streams.py::test_duplicate_stream
+            streams_dict[name] = stream
             # move to next entry in streams table
             stream_entry_rva += stream.stream_table_entry_size()
 
@@ -360,25 +382,25 @@ def parse_stream_table(self, pe: dnPE, streams_table_rva):
 
 
 class ClrStruct(Structure):
-    cb: int = None
-    MajorRuntimeVersion: int = None
-    MinorRuntimeVersion: int = None
-    MetaDataRva: int = None
-    MetaDataSize: int = None
-    Flags: int = None
-    EntryPointTokenOrRva: int = None
-    ResourcesRva: int = None
-    ResourcesSize: int = None
-    StrongNameSignatureRva: int = None
-    StrongNameSignatureSize: int = None
-    CodeManagerTableRva: int = None
-    CodeManagerTableSize: int = None
-    VTableFixupsRva: int = None
-    VTableFixupsSize: int = None
-    ExportAddressTableJumpsRva: int = None
-    ExportAddressTableJumpsSize: int = None
-    ManagedNativeHeaderRva: int = None
-    ManagedNativeHeaderSize: int = None
+    cb: int
+    MajorRuntimeVersion: int
+    MinorRuntimeVersion: int
+    MetaDataRva: int
+    MetaDataSize: int
+    Flags: int
+    EntryPointTokenOrRva: int
+    ResourcesRva: int
+    ResourcesSize: int
+    StrongNameSignatureRva: int
+    StrongNameSignatureSize: int
+    CodeManagerTableRva: int
+    CodeManagerTableSize: int
+    VTableFixupsRva: int
+    VTableFixupsSize: int
+    ExportAddressTableJumpsRva: int
+    ExportAddressTableJumpsSize: int
+    ManagedNativeHeaderRva: int
+    ManagedNativeHeaderSize: int
 
 
 class ClrData(DataContainer):
@@ -387,19 +409,21 @@ class ClrData(DataContainer):
     struct:         IMAGE_NET_DIRECTORY structure
     metadata:       ClrMetaData or None
     strings:        stream.StringsHeap or None
+    user_strings:   stream.UserStringsHeap or None
     guids:          stream.GuidHeap or None
     blobs:          stream.BlobHeap or None
     mdtables:       stream.MetaDataTables or None
     Flags:          enums.ClrHeaderFlags or None
     """
 
-    metadata: ClrMetaData = None
-    struct: ClrStruct = None
-    strings: stream.StringsHeap = None
-    guids: stream.GuidHeap = None
-    blobs: stream.BlobHeap = None
-    mdtables: stream.MetaDataTables = None
-    Flags: enums.ClrHeaderFlags = None
+    struct: ClrStruct
+    metadata: Optional[ClrMetaData]
+    strings: Optional[stream.StringsHeap]
+    user_strings: Optional[stream.UserStringHeap]
+    guids: Optional[stream.GuidHeap]
+    blobs: Optional[stream.BlobHeap]
+    mdtables: Optional[stream.MetaDataTables]
+    Flags: Optional[enums.ClrHeaderFlags]
 
     # Structure description from:
     # http://www.ntcore.com/files/dotnetformat.htm
@@ -441,9 +465,7 @@ def __init__(self, pe: dnPE, rva: int, size: int):
             )
             clr_struct.__unpack__(data)
         except PEFormatError:
-            clr_struct = None
-
-        if not clr_struct:
+            # raise exception cause we can't do anything halfway here.
             raise errors.dnFormatError(
                 "Invalid CLR Structure information. Can't read "
                 "data at RVA: 0x%x" % rva
@@ -454,24 +476,29 @@ def __init__(self, pe: dnPE, rva: int, size: int):
         # parse metadata
         metadata_rva = clr_struct.MetaDataRva
         metadata_size = clr_struct.MetaDataSize
+
         try:
             self.metadata = ClrMetaData(pe, metadata_rva, metadata_size)
         except (errors.dnFormatError, PEFormatError) as e:
+            logger.warning("failed to parse .NET metadata: %s", e)
             self.metadata = None
-            pe.add_warning("Problem parsing .NET metadata")
-            pe.add_warning(str(e))
-        if self.metadata:
-            # create shortcuts for streams
-            # TODO: if there are multiple instances of a type, does dotnet runtime use first?
-            for s in self.metadata.streams_list:
-                if not self.strings and isinstance(s, stream.StringsHeap):
-                    self.strings = s
-                elif not self.guids and isinstance(s, stream.GuidHeap):
-                    self.guids = s
-                elif not self.blobs and isinstance(s, stream.BlobHeap):
-                    self.blobs = s
-                elif not self.mdtables and isinstance(s, stream.MetaDataTables):
-                    self.mdtables: stream.MetaDataTables = s
+            return
+
+        # create shortcuts for streams
+        # dotnet runtime uses the last instance of a type,
+        # see: https://github.com/malwarefrank/dnfile/issues/19#issuecomment-992754448
+        # and test: test_invalid_streams.py::test_duplicate_stream
+        for s in self.metadata.streams_list:
+            if isinstance(s, stream.StringsHeap):
+                self.strings = s
+            elif isinstance(s, stream.UserStringHeap):
+                self.user_strings = s
+            elif isinstance(s, stream.GuidHeap):
+                self.guids = s
+            elif isinstance(s, stream.BlobHeap):
+                self.blobs = s
+            elif isinstance(s, stream.MetaDataTables):
+                self.mdtables: stream.MetaDataTables = s
 
         # Set the flags according to the Flags member
         flags_object = enums.ClrHeaderFlags(clr_struct.Flags)
@@ -499,11 +526,15 @@ class ClrStreamFactory(object):
     @classmethod
     def createStream(
         cls, pe: dnPE, stream_entry_rva: int, metadata_rva: int
-    ) -> base.ClrStream:
+    ) -> Optional[base.ClrStream]:
         # start with structure template
         struct_format = _copymod.deepcopy(cls._template_format)
         # read name
         name = pe.get_string_at_rva(stream_entry_rva + 8)
+        if name is None:
+            logger.warning("failed to read stream name")
+            return None
+
         # round field length up to next 4-byte boundary.  Remember the NULL byte at end.
         name_len = len(name) + (4 - (len(name) % 4))
         # add name field to structure
@@ -521,14 +552,14 @@ def createStream(
         stream_data = pe.get_data(
             metadata_rva + stream_struct.Offset, stream_struct.Size
         )
-        # if there is a subclass for this stream
-        if stream_struct.Name in cls._name_type_map:
-            # use that subclass
-            stream_class = cls._name_type_map[stream_struct.Name]
+        name = stream_struct.Name
+        # use GenericStream for any non-standard streams
+        stream_class = cls._name_type_map.get(name, stream.GenericStream)
+        try:
+            # construct stream, like stream.StreagsHeap ctor or GenericStream ctor
+            s = stream_class(metadata_rva, stream_struct, stream_data)
+        except errors.dnFormatError as e:
+            logger.warning("failed to parse stream: %s", e)
+            return None
         else:
-            # otherwise, use the base stream class
-            stream_class = stream.GenericStream
-        # construct stream
-        s = stream_class(metadata_rva, stream_struct, stream_data)
-        # return stream
-        return s
+            return s
diff --git a/src/dnfile/base.py b/src/dnfile/base.py
index 1db0f64..53e7faa 100644
--- a/src/dnfile/base.py
+++ b/src/dnfile/base.py
@@ -4,17 +4,23 @@
 
 Copyright (c) 2020-2021 MalwareFrank
 """
-
-
 import abc
+import enum
 import struct as _struct
-import collections
-from typing import Dict, List, Type, Tuple, Optional
+import typing
+import logging
+from typing import TYPE_CHECKING, Dict, List, Type, Tuple, Union, Generic, TypeVar, Optional, Sequence
 
 from pefile import Structure
 
 from . import enums, errors
 
+if TYPE_CHECKING:
+    from . import stream
+
+
+logger = logging.getLogger(__name__)
+
 
 class StreamStruct(Structure):
     Name: bytes
@@ -23,19 +29,15 @@ class StreamStruct(Structure):
 
 
 class ClrStream(abc.ABC):
-    struct: StreamStruct
-    rva: int
-    __data__: bytes
-
     def __init__(
         self,
         metadata_rva: int,
         stream_struct: StreamStruct,
         stream_data: bytes,
     ):
-        self.struct = stream_struct
-        self.rva = metadata_rva + stream_struct.Offset
-        self.__data__ = stream_data
+        self.struct: StreamStruct = stream_struct
+        self.rva: int = metadata_rva + stream_struct.Offset
+        self.__data__: bytes = stream_data
         self._stream_table_entry_size = stream_struct.sizeof()
         self._data_size = len(stream_data)
 
@@ -79,6 +81,7 @@ def get_dword_at_rva(self, rva):
 
 
 class ClrHeap(ClrStream):
+    @abc.abstractmethod
     def get(self, index):
         raise NotImplementedError()
 
@@ -94,27 +97,62 @@ class MDTableRow(abc.ABC):
     A Metadata Table row is a simple structure that holds the
     fields and values.
     """
-
-    struct: RowStruct
-    row_size: int = 0
-
-    _format: Tuple
-    _tables: Dict[str, int]
-    _struct_class = Type[RowStruct]
-
-    # maps from struct attribute to object attribute
-    _struct_strings: Dict[str, str] = None
-    _struct_guids: Dict[str, str] = None
-    _struct_blobs: Dict[str, str] = None
-    _struct_asis: Dict[str, str] = None
-    _struct_codedindexes: Dict[
-        str, Tuple[str, Type["CodedIndex"]]
-    ] = None  # also CodedIndex subclass
-    _struct_indexes: Dict[str, Tuple[str, str]] = None  # also Metadata table name
-    _struct_flags: Dict[
-        str, Tuple[str, Type[enums.ClrFlags]]
-    ] = None  # also ClrFlags subclass
-    _struct_lists: Dict[str, Tuple[str, str]] = None  # also Metadata table name
+    #
+    # required properties for subclasses.
+    #
+    # subclasses must define this property,
+    # or __init__ will raise an exception.
+    #
+    # for example:
+    #
+    #   class ModuleRow(MDTableRow):
+    #       _struct_class = ModuleRowStruct
+    #
+    _struct_class: Type[RowStruct]
+
+    #
+    # optional properties for subclasses.
+    #
+    # when a subclass defines one of these properties,
+    # the given fields will be parsed with the appropriate strategy.
+    # these strategy defintions:
+    #   - map from underlying raw struct (e.g. RowStruct class)
+    #   - map to the high-level property (e.g. MDTableRow subclass)
+    #
+    # for example:
+    #
+    #   class ModuleRow(MDTableRow):
+    #       Name: str
+    #       _struct_class = ModuleRowStruct
+    #       _struct_strings = {
+    #           "Name_StringIndex": "Name",
+    #       }
+    #
+    # this strategy causes the parser to:
+    #   1. parse the raw row using `ModuleRowStruct`
+    #   2. fetch field `ModuleRowStruct.Name_StringIndex`
+    #   3. resolve it as a string (due to strategy name)
+    #   4. assign it to field `ModuleRow.Name`
+    #
+    # valid strategies are:
+    #  - asis: map data as-is, possibly change the field name
+    #  - strings: resolve via UserString table
+    #  - guids: resolve via GUID table
+    #  - blobs: resolve via Blob table
+    #  - flags: resolve via given flags
+    #  - enums: resolve via given enums
+    #  - indexes: resolve via given table name
+    #  - lists: resolve many items via given table name
+    #  - codedindexes: resolve via candidate list of tables
+    _struct_strings: Dict[str, str]
+    _struct_guids: Dict[str, str]
+    _struct_blobs: Dict[str, str]
+    _struct_asis: Dict[str, str]
+    _struct_codedindexes: Dict[str, Tuple[str, Type["CodedIndex"]]]  # also CodedIndex subclass
+    _struct_indexes: Dict[str, Tuple[str, str]]                      # also Metadata table name
+    _struct_flags: Dict[str, Tuple[str, Type[enums.ClrFlags]]]       # also ClrFlags subclass
+    _struct_enums: Dict[str, Tuple[str, Type[enum.IntEnum]]]         # also enum.IntEnum subclassA
+    _struct_lists: Dict[str, Tuple[str, str]]                        # also Metadata table name
 
     def __init__(
         self,
@@ -122,37 +160,46 @@ def __init__(
         strings_offset_size: int,
         guid_offset_size: int,
         blob_offset_size: int,
-        strings_heap: ClrHeap,
-        guid_heap: ClrHeap,
-        blob_heap: ClrHeap,
+        strings_heap: Optional["stream.StringsHeap"],
+        guid_heap: Optional["stream.GuidHeap"],
+        blob_heap: Optional["stream.BlobHeap"],
     ):
         """
         Given the tables' row counts and heap info.
+
         Initialize the following attributes:
             row_size    The size, in bytes, of one row.  Calculated from
-                        tables_rowcounts, heap info, and self._format
+                         tables_rowcounts, heap info, and self._format
+            struct      The class use to parse the data.
 
         tables_rowcounts is indexed by table number.  The value is the row count, if it exists, or None.
         """
+        assert hasattr(self.__class__, "_struct_class")
 
         self._tables_rowcnt = tables_rowcounts
-        self._strings = strings_heap
-        self._guids = guid_heap
-        self._blobs = blob_heap
+        self._strings: Optional["stream.StringsHeap"] = strings_heap
+        self._guids: Optional["stream.GuidHeap"] = guid_heap
+        self._blobs: Optional["stream.BlobHeap"] = blob_heap
         self._str_offsz = strings_offset_size
         self._guid_offsz = guid_offset_size
         self._blob_offsz = blob_offset_size
-        self._init_format()
-        self.struct = self._struct_class(format=self._format)
-        self.row_size = self.struct.sizeof()
+        self._format = self._compute_format()
+        self._data: bytes = b""
+
+        # we are cheating here: this isn't technically a RowStruct, but actually a RowStruct subclass.
+        # but few users will likely reach in here, so ATM its not worth fully type annotating.
+        self.struct: RowStruct = self.__class__._struct_class(format=self._format)
+        self.row_size: int = self.struct.sizeof()
 
-    def _init_format(self):
+    @abc.abstractmethod
+    def _compute_format(self) -> Tuple[str, Sequence[str]]:
         """
-        Initialize the structure format.  This is called by the __init__ function (class constructure)
-        and results in the _format Tuple being set according to the tables rowcounts and heap info.
-        The _format Tuple is passed to RowStruct instantiations to calcuate the row size and to parse a row.
+        Compute the structure format.
+        This will be passed to RowStruct instances to calcuate the row size and to parse a row.
+
+        This may raise an exception when the offsets to referenced streams is too large (>4 bytes).
         """
-        pass
+        ...
 
     def set_data(self, data: bytes, offset: int = None):
         """
@@ -162,7 +209,7 @@ def set_data(self, data: bytes, offset: int = None):
         parse() is called after all tables have had parse_rows() called on them.
         """
         self._data = data
-        self.struct = self._struct_class(format=self._format, file_offset=offset)
+        self.struct = self.__class__._struct_class(format=self._format, file_offset=offset)
         self.struct.__unpack__(data)
 
     def parse(self, tables: List["ClrMetaDataTable"], next_row: Optional["MDTableRow"]):
@@ -171,69 +218,96 @@ def parse(self, tables: List["ClrMetaDataTable"], next_row: Optional["MDTableRow
         have been initialized, i.e. parse_rows() has been called on each table in the tables list.
         """
         # if there are any fields to copy as-is
-        if self._struct_asis:
-            for struct_name, attr_name in self._struct_asis.items():
+        if hasattr(self.__class__, "_struct_asis"):
+            for struct_name, attr_name in self.__class__._struct_asis.items():
                 setattr(self, attr_name, getattr(self.struct, struct_name, None))
+
         # if strings
-        if self._struct_strings and self._strings:
-            for struct_name, attr_name in self._struct_strings.items():
+        if hasattr(self.__class__, "_struct_strings"):
+            for struct_name, attr_name in self.__class__._struct_strings.items():
+                if self._strings is None:
+                    logger.warning("failed to fetch string: no strings table")
+                    continue
+
                 i = getattr(self.struct, struct_name, None)
                 try:
                     s = self._strings.get(i)
+                    setattr(self, attr_name, s)
                 except UnicodeDecodeError:
                     s = self._strings.get(i, as_bytes=True)
+                    logger.warning("string: invalid encoding")
+                    setattr(self, attr_name, s)
                 except IndexError:
-                    s = None
-                    # TODO error/warn
-                setattr(self, attr_name, s)
+                    logger.warning("failed to fetch string: unable to parse data")
+
         # if guids
-        if self._struct_guids and self._guids:
-            for struct_name, attr_name in self._struct_guids.items():
+        if hasattr(self.__class__, "_struct_guids"):
+            for struct_name, attr_name in self.__class__._struct_guids.items():
+                if self._guids is None:
+                    logger.warning("failed to fetch guid: no guid table")
+                    continue
+
                 try:
                     g = self._guids.get(getattr(self.struct, struct_name, None))
+                    setattr(self, attr_name, g)
                 except (IndexError, TypeError):
-                    g = None
-                    # TODO error/warn
-                setattr(self, attr_name, g)
+                    logger.warning("failed to fetch guid: unable to parse data")
+
         # if blobs
-        if self._struct_blobs and self._blobs:
-            for struct_name, attr_name in self._struct_blobs.items():
+        if hasattr(self.__class__, "_struct_blobs"):
+            for struct_name, attr_name in self.__class__._struct_blobs.items():
+                if self._blobs is None:
+                    logger.warning("failed to fetch blob: no blob table")
+                    continue
                 try:
                     b = self._blobs.get(getattr(self.struct, struct_name, None))
+                    setattr(self, attr_name, b)
                 except (IndexError, TypeError):
-                    b = None
-                    # TODO error/warn
-                setattr(self, attr_name, b)
+                    logger.warning("failed to fetch blob: unable to parse data")
+
         # if coded indexes
-        if self._struct_codedindexes and tables:
+        if hasattr(self.__class__, "_struct_codedindexes") and tables:
             for struct_name, (
                 attr_name,
                 attr_class,
-            ) in self._struct_codedindexes.items():
+            ) in self.__class__._struct_codedindexes.items():
                 try:
                     o = attr_class(getattr(self.struct, struct_name, None), tables)
+                    setattr(self, attr_name, o)
                 except (IndexError, TypeError):
-                    o = None
-                    # TODO error/warn
-                setattr(self, attr_name, o)
+                    logger.warning("failed to fetch coded index: unable to parse data")
+
         # if flags
-        if self._struct_flags:
-            for struct_name, (attr_name, attr_class) in self._struct_flags.items():
+        if hasattr(self.__class__, "_struct_flags"):
+            for struct_name, (attr_name, flag_class) in self.__class__._struct_flags.items():
                 # Set the flags according to the Flags member
                 v = getattr(self.struct, struct_name, None)
-                if v is not None:
-                    try:
-                        flag_object = attr_class(v)
-                    except ValueError:
-                        flag_object = None
-                        # TODO error/warn
-                else:
-                    flag_object = None
-                    # TODO error/warn
-                setattr(self, attr_name, flag_object)
+                if v is None:
+                    logger.warning("failed to fetch flag: no data")
+                    continue
+
+                try:
+                    setattr(self, attr_name, flag_class(v))
+                except ValueError:
+                    logger.warning("failed to fetch flag: invalid flag data")
+
+        # if enums
+        if hasattr(self.__class__, "_struct_enums"):
+            for struct_name, (attr_name, enum_class) in self.__class__._struct_enums.items():
+                # Set the value according to the Enum member
+                v = getattr(self.struct, struct_name, None)
+                if v is None:
+                    logger.warning("failed to fetch enum: no data")
+                    continue
+
+                try:
+                    setattr(self, attr_name, enum_class(v))
+                except ValueError:
+                    logger.warning("failed to fetch enum: invalid enum data")
+
         # if indexes
-        if self._struct_indexes and tables:
-            for struct_name, (attr_name, table_name) in self._struct_indexes.items():
+        if hasattr(self.__class__, "_struct_indexes") and tables:
+            for struct_name, (attr_name, table_name) in self.__class__._struct_indexes.items():
                 table = None
                 for t in tables:
                     if t.name == table_name:
@@ -241,13 +315,13 @@ def parse(self, tables: List["ClrMetaDataTable"], next_row: Optional["MDTableRow
                 if table:
                     i = getattr(self.struct, struct_name, None)
                     if i is not None and i > 0 and i <= table.num_rows:
-                        setattr(self, attr_name, MDTableIndexRef(table, i))
+                        setattr(self, attr_name, MDTableIndex(table, i))
                     else:
-                        setattr(self, attr_name, None)
-                        # TODO error/warn
+                        logger.warning("failed to fetch index reference: unable to parse data")
+
         # if lists
-        if self._struct_lists and tables:
-            for struct_name, (attr_name, table_name) in self._struct_lists.items():
+        if hasattr(self.__class__, "_struct_lists") and tables:
+            for struct_name, (attr_name, table_name) in self.__class__._struct_lists.items():
                 table = None
                 for t in tables:
                     if t.name == table_name:
@@ -287,7 +361,7 @@ def parse(self, tables: List["ClrMetaDataTable"], next_row: Optional["MDTableRow
                     if (run_start_index != run_end_index) or (run_end_index == max_row):
                         # row indexes are 1-indexed, so our range goes to end+1
                         for row_index in range(run_start_index, run_end_index + 1):
-                            run.append(MDTableIndexRef(table, row_index))
+                            run.append(MDTableIndex(table, row_index))
 
                 setattr(self, attr_name, run)
 
@@ -296,7 +370,7 @@ def _table_name2num(self, name, tables: List["ClrMetaDataTable"]):
             if t.name == name:
                 return t.number
 
-    def _clr_coded_index_struct_size(self, tag_bits, table_names):
+    def _clr_coded_index_struct_size(self, tag_bits: int, table_names: Sequence[str]) -> str:
         """
         Given table names and tag bits, checks for the max row count
         size among the given tables and returns "H" if it will fit in
@@ -306,22 +380,27 @@ def _clr_coded_index_struct_size(self, tag_bits, table_names):
         The returned character can be used in a Structure format or
         passing to struct.pack()
         """
-        if not table_names or not self._tables_rowcnt:
-            # error
-            raise dnFormatError("Problem parsing .NET coded index")
         max_index = 0
         for name in table_names:
             if not name:
                 continue
-            i = enums.MetadataTables[name].value
-            if self._tables_rowcnt[i] > max_index:
-                max_index = self._tables_rowcnt[i]
+
+            table_index = enums.MetadataTables[name].value
+            table_rowcnt = self._tables_rowcnt[table_index]
+            if table_rowcnt is None:
+                # the requested table is not present,
+                # so it effectively has zero rows.
+                table_rowcnt = 0
+
+            max_index = max(max_index, table_rowcnt)
+
         # if it can fit in a word (minus bits for reference id)
         if max_index <= 2 ** (16 - tag_bits):
             # size is a word
             return "H"
-        # otherwise, size is a dword
-        return "I"
+        else:
+            # otherwise, size is a dword
+            return "I"
 
 
 class MDTablesStruct(Structure):
@@ -334,31 +413,109 @@ class MDTablesStruct(Structure):
     MaskSorted: int
 
 
-class ClrMetaDataTable(collections.abc.Sequence):
+# This type describes the type of row that a table contains.
+# For example, the Module table contains ModuleRows,
+# so it inherits like this:
+#
+#     class Module(ClrMetaDataTable[ModuleRow]):
+#         ...
+#
+# This lets us specify that `dnfile.mdtables.Module[0]` is a ModuleRow
+# and therefore has properties Name, Generation, etc.
+#
+# Instances of these types must be subclasses of MDTableRow.
+RowType = TypeVar('RowType', bound=MDTableRow)
+
+
+class MDTableIndex(Generic[RowType]):
+    """
+    An index into a Metadata Table.
+
+    Attributes:
+        table           Table object.
+        row_index       Index number of the row.
+        row             The referenced row.
+    """
+    def __init__(self, table: "ClrMetaDataTable[RowType]", row_index: int):
+        self.table: Optional["ClrMetaDataTable[RowType]"] = table
+        self.row_index: int = row_index
+
+    @property
+    def row(self) -> Optional[RowType]:
+        if self.table is None:
+            return None
+        else:
+            return self.table.get_with_row_index(self.row_index)
+
+
+class CodedIndex(MDTableIndex[RowType]):
+    """
+    Subclasses should be sure to set the following attributes:
+      - tag_bits        Number of bits used to specify the table name index.
+      - table_names     Candidate list of table names.
+    """
+    #
+    # required properties for subclasses.
+    #
+    # subclasses must define this property,
+    # or __init__ will raise an exception.
+    #
+    # for example:
+    #
+    #   class TypeDefOrRef(CodedIndex[...]):
+    #       tag_bits = 2
+    #       table_names = ("TypeDef", "TypeRef", "TypeSpec")
+    #
+    tag_bits: int
+    table_names: Sequence[str]
+
+    def __init__(self, value, tables: List["ClrMetaDataTable[RowType]"]):
+        assert hasattr(self, "tag_bits")
+        assert hasattr(self, "table_names")
+
+        table_name = self.table_names[value & (2 ** self.tag_bits - 1)]
+        self.row_index = value >> self.tag_bits
+
+        for t in tables:
+            if t.name != table_name:
+                continue
+
+            self.table = t
+            return
+
+        logger.warning("reference to missing table: %s" % table_name)
+        self.table = None
+
+
+class ClrMetaDataTable(Generic[RowType]):
     """
     An abstract class for Metadata tables.  Rows can be accessed
-    directly like a list with bracket [] syntax.
+     directly like a list with bracket [] syntax.
+    Use `get_with_row_index` when you have a Rid/token/row_index,
+     since these are 1-indexed.
+    Use bracket [] syntax when you want 0-indexing.
 
     Subclasses should make sure to set the following attributes:
         number
         name
-        _format
-        _flags
         _row_class
     """
-
-    number: int = None
-    name: str = None
-    num_rows: int = 0
-    row_size: int = 0
-    rows: List[MDTableRow] = None
-    is_sorted = False
-    rva: int = 0
-
-    _format: Tuple = None
-    _flags: Tuple = None
-    _row_class: Type[MDTableRow] = None
-    _table_data: bytes
+    #
+    # required properties for subclasses.
+    #
+    # subclasses must define this property,
+    # or __init__ will raise an exception.
+    #
+    # for example:
+    #
+    #   class Module(ClrMetaDataTable[ModuleRow]):
+    #       name = "Module"
+    #       number = 0
+    #       _row_class = ModuleRow
+    #
+    number: int
+    name: str
+    _row_class: Type[RowType]
 
     def __init__(
         self,
@@ -367,9 +524,9 @@ def __init__(
         strings_offset_size: int,
         guid_offset_size: int,
         blob_offset_size: int,
-        strings_heap: ClrHeap,
-        guid_heap: ClrHeap,
-        blob_heap: ClrHeap,
+        strings_heap: Optional["stream.StringsHeap"],
+        guid_heap: Optional["stream.GuidHeap"],
+        blob_heap: Optional["stream.BlobHeap"],
     ):
         """
         Given the tables' row counts, sorted flag, and heap info.
@@ -380,36 +537,48 @@ def __init__(
             is_sorted   Whether the table is sorted, according to tables_info.
             rows        Initialized but not-yet-parsed list of rows.
         """
-        self.is_sorted = is_sorted
-        self.num_rows = tables_rowcounts[self.number]
-        self.rows = list()
+        assert hasattr(self, "number")
+        assert hasattr(self, "name")
+        assert hasattr(self, "_row_class")
+
+        num_rows = tables_rowcounts[self.number]
+        if num_rows is None:
+            # the table doesn't exist, so create the instance, but with zero rows.
+            logger.warning("reference to missing table: %d" % (self.number))
+            num_rows = 0
+
+        self.is_sorted: bool = is_sorted
+        self.num_rows: int = num_rows
+
+        self.rows: List[RowType] = []
+        for i in range(num_rows):
+            try:
+                self.rows.append(self._row_class(
+                    tables_rowcounts,
+                    strings_offset_size,
+                    guid_offset_size,
+                    blob_offset_size,
+                    strings_heap,
+                    guid_heap,
+                    blob_heap,
+                ))
+            except errors.dnFormatError:
+                # this may occur when the offset to a stream is too large.
+                # this probably means invalid data.
+                logger.warning("failed to construct %s row %d", self.name, i)
+                break
+
         # store heap info
-        self._strings_heap = strings_heap
-        self._guid_heap = guid_heap
-        self._blob_heap = blob_heap
+        self._strings_heap: Optional["stream.StringsHeap"] = strings_heap
+        self._guid_heap: Optional["stream.GuidHeap"] = guid_heap
+        self._blob_heap: Optional["stream.BlobHeap"] = blob_heap
         self._strings_offset_size = strings_offset_size
         self._guid_offset_size = guid_offset_size
         self._blob_offset_size = blob_offset_size
         self._tables_rowcounts = tables_rowcounts
-        # init rows
-        self._init_rows()
-        # get row size
-        self.row_size = self._get_row_size()
-
-    def _init_rows(self):
-        if self._row_class:
-            for i in range(self.num_rows):
-                r: MDTableRow
-                r = self._row_class(
-                    self._tables_rowcounts,
-                    self._strings_offset_size,
-                    self._guid_offset_size,
-                    self._blob_offset_size,
-                    self._strings_heap,
-                    self._guid_heap,
-                    self._blob_heap,
-                )
-                self.rows.append(r)
+
+        self._table_data: bytes = b""
+        self.row_size: int = self._get_row_size()
 
     def _get_row_size(self):
         if not self.rows:
@@ -417,7 +586,7 @@ def _get_row_size(self):
         r = self.rows[0]
         return r.row_size
 
-    def parse_rows(self, data: bytes):
+    def parse_rows(self, table_rva: int, data: bytes):
         """
         Given a byte sequence containing the rows, add data to each row in the
         self.rows list.  Note that the rows have not been fully parsed until
@@ -426,24 +595,18 @@ def parse_rows(self, data: bytes):
         """
         self._table_data = data
         if len(data) < self.row_size * self.num_rows:
-            # error/warn
-            raise errors.dnFormatError(
-                "Error parsing table {}, len(data)={}  row_size={}  num_rows={}".format(
-                    self.name, len(data), self.row_size, self.num_rows
-                )
-            )
+            logger.warning("not enough data to parse %d rows", self.num_rows)
+            # we can still try to parse some of the rows...
+
         offset = 0
         # iterate through rows, stopping at num_rows or when there is not enough data left
         for i in range(self.num_rows):
             if len(data) < offset + self.row_size:
-                # error/warn
-                raise errors.dnFormatError(
-                    "Error parsing row {} for table {}, len(data)={}  row_size={}  offset={}".format(
-                        i, self.name, len(data), self.row_size, offset
-                    )
-                )
+                logger.warning("not enough data to parse row %d", i)
+                break
+
             self.rows[i].set_data(
-                data[offset:offset + self.row_size], offset=self.rva + offset
+                data[offset:offset + self.row_size], offset=table_rva + offset
             )
             offset += self.row_size
 
@@ -457,21 +620,28 @@ def parse(self, tables: List["ClrMetaDataTable"]):
         """
 
         # for each row in table
-        for i, r in enumerate(self.rows):
+        for i, row in enumerate(self.rows):
             next_row = None
             if i + 1 < len(self.rows):
                 next_row = self.rows[i + 1]
 
             # fully parse the row
-            r.parse(tables, next_row=next_row)
+            row.parse(tables, next_row=next_row)
 
-    def __getitem__(self, index: int):
+    def __getitem__(self, index: int) -> RowType:
         return self.rows[index]
 
     def __len__(self):
+        """
+        the actual number of rows parsed,
+        as opposed to self.num_rows, which is the declared row count of the table
+        """
         return len(self.rows)
 
-    def get_with_row_index(self, row_index: int):
+    def __iter__(self):
+        return iter(self.rows)
+
+    def get_with_row_index(self, row_index: int) -> RowType:
         """
         fetch the row with the given row index.
         remember: row indices, at least those encoded within a .NET file, are 1-based.
@@ -479,41 +649,3 @@ def get_with_row_index(self, row_index: int):
         use `__getitem__` when you want 0-based indexing.
         """
         return self[row_index - 1]
-
-
-class MDTableIndex(object):
-    """
-    An index into a Metadata Table.
-
-    Attributes:
-        table           Table object.
-        row_index       Index number of the row.
-    """
-
-    table: ClrMetaDataTable
-    row_index: int
-    row: MDTableRow
-
-
-class CodedIndex(MDTableIndex):
-    tag_bits: int
-    table_names: Tuple[str]
-
-    def __init__(self, value, tables_list: List[ClrMetaDataTable]):
-        table_name = self.table_names[value & (2 ** self.tag_bits - 1)]
-        self.row_index = value >> self.tag_bits
-        for t in tables_list:
-            if t.name == table_name:
-                self.table = t
-                if self.row_index > t.num_rows:
-                    # TODO error/warn
-                    self.row = None
-                    return
-                self.row = t.rows[self.row_index - 1]
-
-
-class MDTableIndexRef(MDTableIndex):
-    def __init__(self, table, row_index):
-        self.table = table
-        self.row_index = row_index
-        self.row = table.get_with_row_index(row_index)
diff --git a/src/dnfile/codedindex.py b/src/dnfile/codedindex.py
index ea548c7..dab9452 100644
--- a/src/dnfile/codedindex.py
+++ b/src/dnfile/codedindex.py
@@ -12,26 +12,75 @@
 Copyright (c) 2020-2021 MalwareFrank
 """
 
-from typing import List, Tuple
+from typing import TYPE_CHECKING, Union
 
 from .base import CodedIndex
 
-# TODO add table_numbers to all
+if TYPE_CHECKING:
+    from .mdtable import (
+        FileRow,
+        EventRow,
+        FieldRow,
+        ParamRow,
+        ModuleRow,
+        TypeDefRow,
+        TypeRefRow,
+        AssemblyRow,
+        PropertyRow,
+        TypeSpecRow,
+        MemberRefRow,
+        MethodDefRow,
+        ModuleRefRow,
+        AssemblyRefRow,
+        DeclSecurityRow,
+        ExportedTypeRow,
+        GenericParamRow,
+        InterfaceImplRow,
+        StandAloneSigRow,
+        ManifestResourceRow,
+        GenericParamConstraintRow,
+    )
 
 
-class TypeDefOrRef(CodedIndex):
+class TypeDefOrRef(CodedIndex[Union["TypeDefRow", "TypeRefRow", "TypeSpecRow"]]):
     tag_bits = 2
     table_names = ("TypeDef", "TypeRef", "TypeSpec")
     table_numbers = (2, 1, 27)
 
 
-class HasConstant(CodedIndex):
+class HasConstant(CodedIndex[Union["FieldRow", "ParamRow", "PropertyRow"]]):
     tag_bits = 2
     table_names = ("Field", "Param", "Property")
     table_numbers = (4, 8, 23)
 
 
-class HasCustomAttribute(CodedIndex):
+class HasCustomAttribute(
+    CodedIndex[
+        Union[
+            "MethodDefRow",
+            "FieldRow",
+            "TypeRefRow",
+            "TypeDefRow",
+            "ParamRow",
+            "InterfaceImplRow",
+            "MemberRefRow",
+            "ModuleRow",
+            "DeclSecurityRow",
+            "PropertyRow",
+            "EventRow",
+            "StandAloneSigRow",
+            "ModuleRefRow",
+            "TypeSpecRow",
+            "AssemblyRow",
+            "AssemblyRefRow",
+            "FileRow",
+            "ExportedTypeRow",
+            "ManifestResourceRow",
+            "GenericParamRow",
+            "GenericParamConstraintRow",
+        ]
+    ]
+):
     tag_bits = 5
     # TODO this may be an incomplete list
     table_names = (
@@ -59,51 +108,51 @@ class HasCustomAttribute(CodedIndex):
     )
 
 
-class HasFieldMarshall(CodedIndex):
+class HasFieldMarshall(CodedIndex[Union["FieldRow", "ParamRow"]]):
     tag_bits = 1
     table_names = ("Field", "Param")
 
 
-class HasDeclSecurity(CodedIndex):
+class HasDeclSecurity(CodedIndex[Union["TypeDefRow", "MethodDefRow", "AssemblyRow"]]):
     tag_bits = 2
     table_names = ("TypeDef", "MethodDef", "Assembly")
 
 
-class MemberRefParent(CodedIndex):
+class MemberRefParent(CodedIndex[Union["TypeDefRow", "TypeRefRow", "ModuleRefRow", "MethodDefRow", "TypeSpecRow"]]):
     tag_bits = 3
     table_names = ("TypeDef", "TypeRef", "ModuleRef", "MethodDef", "TypeSpec")
 
 
-class HasSemantics(CodedIndex):
+class HasSemantics(CodedIndex[Union["EventRow", "PropertyRow"]]):
     tag_bits = 1
     table_names = ("Event", "Property")
 
 
-class MethodDefOrRef(CodedIndex):
+class MethodDefOrRef(CodedIndex[Union["MethodDefRow", "MemberRefRow"]]):
     tag_bits = 1
     table_names = ("MethodDef", "MemberRef")
 
 
-class MemberForwarded(CodedIndex):
+class MemberForwarded(CodedIndex[Union["FieldRow", "MethodDefRow"]]):
     tag_bits = 1
     table_names = ("Field", "MethodDef")
 
 
-class Implementation(CodedIndex):
+class Implementation(CodedIndex[Union["FileRow", "AssemblyRefRow", "ExportedTypeRow"]]):
     tag_bits = 2
     table_names = ("File", "AssemblyRef", "ExportedType")
 
 
-class CustomAttributeType(CodedIndex):
+class CustomAttributeType(CodedIndex[Union["MethodDefRow", "MemberRefRow"]]):
     tag_bits = 3
-    table_names = (None, None, "MethodDef", "MemberRef", None)
+    table_names = ("Unused", "Unused", "MethodDef", "MemberRef", "Unused")
 
 
-class ResolutionScope(CodedIndex):
+class ResolutionScope(CodedIndex[Union["ModuleRow", "ModuleRefRow", "AssemblyRefRow", "TypeRefRow"]]):
     tag_bits = 2
     table_names = ("Module", "ModuleRef", "AssemblyRef", "TypeRef")
 
 
-class TypeOrMethodDef(CodedIndex):
+class TypeOrMethodDef(CodedIndex[Union["TypeDefRow", "MethodDefRow"]]):
     tag_bits = 1
     table_names = ("TypeDef", "MethodDef")
diff --git a/src/dnfile/enums.py b/src/dnfile/enums.py
index 966b13c..5c21226 100644
--- a/src/dnfile/enums.py
+++ b/src/dnfile/enums.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 
 import enum as _enum
-from typing import Dict, Iterable
+from typing import Dict, Type, Iterable
 
 ########
 # Most developers may just use the Clr* classes to automatically parse the
@@ -36,18 +36,18 @@ class ClrFlags(object):
     :var _flags:        an iterable of classes defining bit flags to check and set if set.
     """
 
-    corhdr_enum: ClrMetaDataEnum = None
-    _masks: Dict[str, _enum.Enum] = None
-    _flags: Iterable[_enum.Enum] = None
+    corhdr_enum: Type[ClrMetaDataEnum]
+    _masks: Dict[str, Type[_enum.IntEnum]]
+    _flags: Iterable[Type[_enum.IntEnum]]
 
     def __init__(self, value):
-        if self._masks:
+        if hasattr(self, "_masks"):
             for mask_name, value_class in self._masks.items():
                 chk = getattr(self.corhdr_enum, mask_name) & value
                 val = value_class(chk)
                 setattr(self, val.name, True)
 
-        if self._flags:
+        if hasattr(self, "_flags"):
             for value_class in self._flags:
                 for m in value_class:
                     if m.value & value:
@@ -844,6 +844,7 @@ class MetadataTables(_enum.IntEnum):
     GenericMethod = 43
     GenericParamConstraint = 44
     # 45 through 63 are not used
+    Unused = 62
     MaxTable = 63
 
 
diff --git a/src/dnfile/mdtable.py b/src/dnfile/mdtable.py
index c0e05cf..386dad8 100644
--- a/src/dnfile/mdtable.py
+++ b/src/dnfile/mdtable.py
@@ -11,35 +11,63 @@
 
 Copyright (c) 2020-2021 MalwareFrank
 """
+from typing import TYPE_CHECKING, Dict, List, Type, Optional
 
+from . import enums, utils, errors, codedindex
+from .base import RowStruct, MDTableRow, MDTableIndex, ClrMetaDataTable
 
-from typing import List, Type
+if TYPE_CHECKING:
+    from . import stream
+
+
+def checked_offset_format(offset: int):
+    """
+    compute the format specifier needed for the given offset value.
+    raises an exception if the offset cannot be represented.
+    """
+
+    # implementation: this exception will propagate up
+    # `_compute_format` to `MDTableRow.__init__`
+    # and halt population of a table's row.
+    format = utils.num_bytes_to_struct_char(offset)
+    if format is None:
+        raise errors.dnFormatError("invalid offset: value too large")
+    return format
 
-from . import enums, utils, codedindex
-from .base import ClrHeap, RowStruct, MDTableRow, ClrMetaDataTable
 
 #### Module Table
 #
 
 
 class ModuleRowStruct(RowStruct):
-    Generation: int = None
-    Name_StringIndex: int = None
-    Mvid_GuidIndex: int = None
-    EncId_GuidIndex: int = None
-    EncBaseId_GuidIndex: int = None
+    #
+    # these are type hints for properties dynamically set during structure parsing.
+    #
+    Generation: int
+    Name_StringIndex: int
+    Mvid_GuidIndex: int
+    EncId_GuidIndex: int
+    EncBaseId_GuidIndex: int
 
 
 class ModuleRow(MDTableRow):
+    #
+    # these are type hints for properties dynamically set during structure parsing.
+    #
     Generation: int
     Name: str
     Mvid: str
     EncId: str
     EncBaseId: str
 
-    struct: ModuleRowStruct
+    #
+    # raw structure definition
+    #
     _struct_class = ModuleRowStruct
 
+    #
+    # parsing strategies
+    #
     _struct_asis = {"Generation": "Generation"}
     _struct_strings = {
         "Name_StringIndex": "Name",
@@ -50,10 +78,10 @@ class ModuleRow(MDTableRow):
         "EncBaseId_GuidIndex": "EncBaseId",
     }
 
-    def _init_format(self):
-        str_ind_size = utils.num_bytes_to_struct_char(self._str_offsz)
-        guid_ind_size = utils.num_bytes_to_struct_char(self._guid_offsz)
-        self._format = (
+    def _compute_format(self):
+        str_ind_size = checked_offset_format(self._str_offsz)
+        guid_ind_size = checked_offset_format(self._guid_offsz)
+        return (
             "CLR_METADATA_TABLE_MODULE",
             (
                 "H,Generation",
@@ -65,7 +93,7 @@ def _init_format(self):
         )
 
 
-class Module(ClrMetaDataTable):
+class Module(ClrMetaDataTable[ModuleRow]):
     name = "Module"
     number = 0
 
@@ -77,9 +105,9 @@ class Module(ClrMetaDataTable):
 
 
 class TypeRefRowStruct(RowStruct):
-    ResolutionScope_CodedIndex: int = None
-    TypeName_StringIndex: int = None
-    TypeNamespace_StringIndex: int = None
+    ResolutionScope_CodedIndex: int
+    TypeName_StringIndex: int
+    TypeNamespace_StringIndex: int
 
 
 class TypeRefRow(MDTableRow):
@@ -87,7 +115,6 @@ class TypeRefRow(MDTableRow):
     TypeName: str
     TypeNamespace: str
 
-    struct: TypeRefRowStruct
     _struct_class = TypeRefRowStruct
 
     _struct_strings = {
@@ -98,13 +125,13 @@ class TypeRefRow(MDTableRow):
         "ResolutionScope_CodedIndex": ("ResolutionScope", codedindex.ResolutionScope),
     }
 
-    def _init_format(self):
+    def _compute_format(self):
         resolutionscope_size = self._clr_coded_index_struct_size(
             codedindex.ResolutionScope.tag_bits,
             codedindex.ResolutionScope.table_names,
         )
-        str_ind_size = utils.num_bytes_to_struct_char(self._str_offsz)
-        self._format = (
+        str_ind_size = checked_offset_format(self._str_offsz)
+        return (
             "CLR_METADATA_TABLE_TYPEREF",
             (
                 resolutionscope_size + ",ResolutionScope_CodedIndex",
@@ -114,7 +141,7 @@ def _init_format(self):
         )
 
 
-class TypeRef(ClrMetaDataTable):
+class TypeRef(ClrMetaDataTable[TypeRefRow]):
     name = "TypeRef"
     number = 1
 
@@ -126,12 +153,12 @@ class TypeRef(ClrMetaDataTable):
 
 
 class TypeDefRowStruct(RowStruct):
-    Flags: int = None
-    TypeName_StringIndex: int = None
-    TypeNamespace_StringIndex: int = None
-    Extends_CodedIndex: int = None
-    FieldList_Index: int = None
-    MethodList_Index: int = None
+    Flags: int
+    TypeName_StringIndex: int
+    TypeNamespace_StringIndex: int
+    Extends_CodedIndex: int
+    FieldList_Index: int
+    MethodList_Index: int
 
 
 class TypeDefRow(MDTableRow):
@@ -142,7 +169,6 @@ class TypeDefRow(MDTableRow):
     FieldList: List
     MethodList: List
 
-    struct: TypeDefRowStruct
     _struct_class = TypeDefRowStruct
 
     _struct_strings = {
@@ -160,15 +186,15 @@ class TypeDefRow(MDTableRow):
         "MethodList_Index": ("MethodList", "MethodDef"),
     }
 
-    def _init_format(self):
+    def _compute_format(self):
         extends_size = self._clr_coded_index_struct_size(
             codedindex.TypeDefOrRef.tag_bits,
             codedindex.TypeDefOrRef.table_names,
         )
-        str_ind_size = utils.num_bytes_to_struct_char(self._str_offsz)
+        str_ind_size = checked_offset_format(self._str_offsz)
         fieldlist_size = self._clr_coded_index_struct_size(0, ("Field",))
         methodlist_size = self._clr_coded_index_struct_size(0, ("MethodDef",))
-        self._format = (
+        return (
             "CLR_METADATA_TABLE_TYPEDEF",
             (
                 "I,Flags",
@@ -181,7 +207,7 @@ def _init_format(self):
         )
 
 
-class TypeDef(ClrMetaDataTable):
+class TypeDef(ClrMetaDataTable[TypeDefRow]):
     name = "TypeDef"
     number = 2
 
@@ -203,9 +229,9 @@ class FieldPtr(ClrMetaDataTable):
 
 
 class FieldRowStruct(RowStruct):
-    Flags: int = None
-    Name_StringIndex: int = None
-    Signature_BlobIndex: int = None
+    Flags: int
+    Name_StringIndex: int
+    Signature_BlobIndex: int
 
 
 class FieldRow(MDTableRow):
@@ -213,7 +239,6 @@ class FieldRow(MDTableRow):
     Name: str
     Signature: bytes
 
-    struct: FieldRowStruct
     _struct_class = FieldRowStruct
 
     _struct_flags = {
@@ -226,10 +251,10 @@ class FieldRow(MDTableRow):
         "Signature_BlobIndex": "Signature",
     }
 
-    def _init_format(self):
-        str_ind_size = utils.num_bytes_to_struct_char(self._str_offsz)
-        blob_ind_size = utils.num_bytes_to_struct_char(self._blob_offsz)
-        self._format = (
+    def _compute_format(self):
+        str_ind_size = checked_offset_format(self._str_offsz)
+        blob_ind_size = checked_offset_format(self._blob_offsz)
+        return (
             "CLR_METADATA_TABLE_FIELD",
             (
                 "H,Flags",
@@ -261,12 +286,12 @@ class MethodPtr(ClrMetaDataTable):
 
 
 class MethodDefRowStruct(RowStruct):
-    Rva: int = None
-    ImplFlags: int = None
-    Flags: int = None
-    Name_StringIndex: int = None
-    Signature_BlobIndex: int = None
-    ParamList_Index: int = None
+    Rva: int
+    ImplFlags: int
+    Flags: int
+    Name_StringIndex: int
+    Signature_BlobIndex: int
+    ParamList_Index: int
 
 
 class MethodDefRow(MDTableRow):
@@ -275,9 +300,8 @@ class MethodDefRow(MDTableRow):
     Flags: enums.ClrMethodAttr
     Name: str
     Signature: bytes
-    ParamList: List["ParamRow"]
+    ParamList: List[MDTableIndex["ParamRow"]]
 
-    struct: MethodDefRowStruct
     _struct_class = MethodDefRowStruct
 
     _struct_asis = {
@@ -297,11 +321,11 @@ class MethodDefRow(MDTableRow):
         "ParamList_Index": ("ParamList", "Param"),
     }
 
-    def _init_format(self):
-        str_ind_size = utils.num_bytes_to_struct_char(self._str_offsz)
-        blob_ind_size = utils.num_bytes_to_struct_char(self._blob_offsz)
+    def _compute_format(self):
+        str_ind_size = checked_offset_format(self._str_offsz)
+        blob_ind_size = checked_offset_format(self._blob_offsz)
         paramlist_size = self._clr_coded_index_struct_size(0, ("Param",))
-        self._format = (
+        return (
             "CLR_METADATA_TABLE_METHODDEF",
             (
                 "I,Rva",
@@ -314,7 +338,7 @@ def _init_format(self):
         )
 
 
-class MethodDef(ClrMetaDataTable):
+class MethodDef(ClrMetaDataTable[MethodDefRow]):
     name = "MethodDef"
     number = 6
 
@@ -336,9 +360,9 @@ class ParamPtr(ClrMetaDataTable):
 
 
 class ParamRowStruct(RowStruct):
-    Flags: int = None
-    Sequence: int = None
-    Name_StringIndex: int = None
+    Flags: int
+    Sequence: int
+    Name_StringIndex: int
 
 
 class ParamRow(MDTableRow):
@@ -346,7 +370,6 @@ class ParamRow(MDTableRow):
     Sequence: int
     Name: str
 
-    struct: ParamRowStruct
     _struct_class = ParamRowStruct
 
     _struct_flags = {
@@ -359,9 +382,9 @@ class ParamRow(MDTableRow):
         "Name_StringIndex": "Name",
     }
 
-    def _init_format(self):
-        str_ind_size = utils.num_bytes_to_struct_char(self._str_offsz)
-        self._format = (
+    def _compute_format(self):
+        str_ind_size = checked_offset_format(self._str_offsz)
+        return (
             "CLR_METADATA_TABLE_PARAM",
             (
                 "H,Flags",
@@ -371,7 +394,7 @@ def _init_format(self):
         )
 
 
-class Param(ClrMetaDataTable):
+class Param(ClrMetaDataTable[ParamRow]):
     name = "Param"
     number = 8
 
@@ -383,15 +406,14 @@ class Param(ClrMetaDataTable):
 
 
 class InterfaceImplRowStruct(RowStruct):
-    Class_Index: int = None
-    Interface_CodedIndex: int = None
+    Class_Index: int
+    Interface_CodedIndex: int
 
 
 class InterfaceImplRow(MDTableRow):
-    Class: TypeDefRow
+    Class: MDTableIndex[TypeDefRow]
     Interface: codedindex.TypeDefOrRef
 
-    struct: InterfaceImplRowStruct
     _struct_class = InterfaceImplRowStruct
 
     _struct_indexes = {
@@ -401,19 +423,19 @@ class InterfaceImplRow(MDTableRow):
         "Interface_CodedIndex": ("Interface", codedindex.TypeDefOrRef),
     }
 
-    def _init_format(self):
+    def _compute_format(self):
         interface_size = self._clr_coded_index_struct_size(
             codedindex.TypeDefOrRef.tag_bits,
             codedindex.TypeDefOrRef.table_names,
         )
         class_size = self._clr_coded_index_struct_size(0, ("TypeDef",))
-        self._format = (
+        return (
             "CLR_METADATA_TABLE_INTERFACEIMPL",
             (class_size + ",Class_Index", interface_size + ",Interface_CodedIndex"),
         )
 
 
-class InterfaceImpl(ClrMetaDataTable):
+class InterfaceImpl(ClrMetaDataTable[InterfaceImplRow]):
     name = "InterfaceImpl"
     number = 9
 
@@ -425,9 +447,9 @@ class InterfaceImpl(ClrMetaDataTable):
 
 
 class MemberRefRowStruct(RowStruct):
-    Class_CodedIndex: int = None
-    Name_StringIndex: int = None
-    Signature_BlobIndex: int = None
+    Class_CodedIndex: int
+    Name_StringIndex: int
+    Signature_BlobIndex: int
 
 
 class MemberRefRow(MDTableRow):
@@ -435,7 +457,6 @@ class MemberRefRow(MDTableRow):
     Name: str
     Signature: bytes
 
-    struct: MemberRefRowStruct
     _struct_class = MemberRefRowStruct
 
     _struct_codedindexes = {
@@ -448,14 +469,14 @@ class MemberRefRow(MDTableRow):
         "Signature_BlobIndex": "Signature",
     }
 
-    def _init_format(self):
+    def _compute_format(self):
         class_size = self._clr_coded_index_struct_size(
             codedindex.MemberRefParent.tag_bits,
             codedindex.MemberRefParent.table_names,
         )
-        str_ind_size = utils.num_bytes_to_struct_char(self._str_offsz)
-        blob_ind_size = utils.num_bytes_to_struct_char(self._blob_offsz)
-        self._format = (
+        str_ind_size = checked_offset_format(self._str_offsz)
+        blob_ind_size = checked_offset_format(self._blob_offsz)
+        return (
             "CLR_METADATA_TABLE_MEMBERREF",
             (
                 class_size + ",Class_CodedIndex",
@@ -465,7 +486,7 @@ def _init_format(self):
         )
 
 
-class MemberRef(ClrMetaDataTable):
+class MemberRef(ClrMetaDataTable[MemberRefRow]):
     name = "MemberRef"
     number = 10
 
@@ -479,10 +500,10 @@ class MemberRef(ClrMetaDataTable):
 
 
 class ConstantRowStruct(RowStruct):
-    Type: int = None
-    Padding: int = None
-    Parent_CodedIndex: int = None
-    Value_BlobIndex: int = None
+    Type: int
+    Padding: int
+    Parent_CodedIndex: int
+    Value_BlobIndex: int
 
 
 class ConstantRow(MDTableRow):
@@ -491,7 +512,6 @@ class ConstantRow(MDTableRow):
     Parent: codedindex.HasConstant
     Value: bytes
 
-    struct: ConstantRowStruct
     _struct_class = ConstantRowStruct
 
     _struct_asis = {
@@ -505,13 +525,13 @@ class ConstantRow(MDTableRow):
         "Value_BlobIndex": "Value",
     }
 
-    def _init_format(self):
+    def _compute_format(self):
         parent_size = self._clr_coded_index_struct_size(
             codedindex.HasConstant.tag_bits,
             codedindex.HasConstant.table_names,
         )
-        blob_ind_size = utils.num_bytes_to_struct_char(self._blob_offsz)
-        self._format = (
+        blob_ind_size = checked_offset_format(self._blob_offsz)
+        return (
             "CLR_METADATA_TABLE_CONSTANT",
             (
                 "B,Type",
@@ -522,7 +542,7 @@ def _init_format(self):
         )
 
 
-class Constant(ClrMetaDataTable):
+class Constant(ClrMetaDataTable[ConstantRow]):
     name = "Constant"
     number = 11
 
@@ -536,9 +556,9 @@ class Constant(ClrMetaDataTable):
 
 
 class CustomAttributeRowStruct(RowStruct):
-    Parent_CodedIndex: int = None
-    Type_CodedIndex: int = None
-    Value_BlobIndex: int = None
+    Parent_CodedIndex: int
+    Type_CodedIndex: int
+    Value_BlobIndex: int
 
 
 class CustomAttributeRow(MDTableRow):
@@ -546,7 +566,6 @@ class CustomAttributeRow(MDTableRow):
     Type: codedindex.CustomAttributeType
     Value: bytes
 
-    struct: CustomAttributeRowStruct
     _struct_class = CustomAttributeRowStruct
 
     _struct_codedindexes = {
@@ -557,7 +576,7 @@ class CustomAttributeRow(MDTableRow):
         "Value_BlobIndex": "Value",
     }
 
-    def _init_format(self):
+    def _compute_format(self):
         parent_size = self._clr_coded_index_struct_size(
             codedindex.HasCustomAttribute.tag_bits,
             codedindex.HasCustomAttribute.table_names,
@@ -566,8 +585,8 @@ def _init_format(self):
             codedindex.CustomAttributeType.tag_bits,
             codedindex.CustomAttributeType.table_names,
         )
-        blob_ind_size = utils.num_bytes_to_struct_char(self._blob_offsz)
-        self._format = (
+        blob_ind_size = checked_offset_format(self._blob_offsz)
+        return (
             "CLR_METADATA_TABLE_CUSTOMATTRIBUTE",
             (
                 parent_size + ",Parent_CodedIndex",
@@ -577,7 +596,7 @@ def _init_format(self):
         )
 
 
-class CustomAttribute(ClrMetaDataTable):
+class CustomAttribute(ClrMetaDataTable[CustomAttributeRow]):
     name = "CustomAttribute"
     number = 12
 
@@ -589,15 +608,14 @@ class CustomAttribute(ClrMetaDataTable):
 
 
 class FieldMarshalRowStruct(RowStruct):
-    Parent_CodedIndex: int = None
-    NativeType_BlobIndex: int = None
+    Parent_CodedIndex: int
+    NativeType_BlobIndex: int
 
 
 class FieldMarshalRow(MDTableRow):
     Parent: codedindex.HasFieldMarshall
     NativeType: bytes
 
-    struct: FieldMarshalRowStruct
     _struct_class = FieldMarshalRowStruct
 
     _struct_codedindexes = {
@@ -607,13 +625,13 @@ class FieldMarshalRow(MDTableRow):
         "NativeType_BlobIndex": "NativeType",
     }
 
-    def _init_format(self):
+    def _compute_format(self):
         parent_size = self._clr_coded_index_struct_size(
             codedindex.HasFieldMarshall.tag_bits,
             codedindex.HasFieldMarshall.table_names,
         )
-        blob_ind_size = utils.num_bytes_to_struct_char(self._blob_offsz)
-        self._format = (
+        blob_ind_size = checked_offset_format(self._blob_offsz)
+        return (
             "CLR_METADATA_TABLE_FIELDMARSHAL",
             (
                 parent_size + ",Parent_CodedIndex",
@@ -622,7 +640,7 @@ def _init_format(self):
         )
 
 
-class FieldMarshal(ClrMetaDataTable):
+class FieldMarshal(ClrMetaDataTable[FieldMarshalRow]):
     name = "FieldMarshal"
     number = 13
 
@@ -634,9 +652,9 @@ class FieldMarshal(ClrMetaDataTable):
 
 
 class DeclSecurityRowStruct(RowStruct):
-    Action: int = None
-    Parent_CodedIndex: int = None
-    PermissionSet_BlobIndex: int = None
+    Action: int
+    Parent_CodedIndex: int
+    PermissionSet_BlobIndex: int
 
 
 class DeclSecurityRow(MDTableRow):
@@ -644,7 +662,6 @@ class DeclSecurityRow(MDTableRow):
     Parent: codedindex.HasDeclSecurity
     PermissionSet: bytes
 
-    struct: DeclSecurityRowStruct
     _struct_class = DeclSecurityRowStruct
 
     _struct_asis = {
@@ -657,13 +674,13 @@ class DeclSecurityRow(MDTableRow):
         "PermissionSet_BlobIndex": "PermissionSet",
     }
 
-    def _init_format(self):
+    def _compute_format(self):
         parent_size = self._clr_coded_index_struct_size(
             codedindex.HasDeclSecurity.tag_bits,
             codedindex.HasDeclSecurity.table_names,
         )
-        blob_ind_size = utils.num_bytes_to_struct_char(self._blob_offsz)
-        self._format = (
+        blob_ind_size = checked_offset_format(self._blob_offsz)
+        return (
             "CLR_METADATA_TABLE_DECLSECURITY",
             (
                 "H,Action",
@@ -673,7 +690,7 @@ def _init_format(self):
         )
 
 
-class DeclSecurity(ClrMetaDataTable):
+class DeclSecurity(ClrMetaDataTable[DeclSecurityRow]):
     name = "DeclSecurity"
     number = 14
 
@@ -685,17 +702,16 @@ class DeclSecurity(ClrMetaDataTable):
 
 
 class ClassLayoutRowStruct(RowStruct):
-    PackingSize: int = None
-    ClassSize: int = None
-    Parent_Index: int = None
+    PackingSize: int
+    ClassSize: int
+    Parent_Index: int
 
 
 class ClassLayoutRow(MDTableRow):
     PackingSize: int
     ClassSize: int
-    Parent: TypeDefRow
+    Parent: MDTableIndex[TypeDefRow]
 
-    struct: ClassLayoutRowStruct
     _struct_class = ClassLayoutRowStruct
 
     _struct_asis = {
@@ -706,9 +722,9 @@ class ClassLayoutRow(MDTableRow):
         "Parent_Index": ("Parent", "TypeDef"),
     }
 
-    def _init_format(self):
+    def _compute_format(self):
         parent_size = self._clr_coded_index_struct_size(0, ("TypeDef",))
-        self._format = (
+        return (
             "CLR_METADATA_TABLE_CLASSLAYOUT",
             (
                 "H,PackingSize",
@@ -718,7 +734,7 @@ def _init_format(self):
         )
 
 
-class ClassLayout(ClrMetaDataTable):
+class ClassLayout(ClrMetaDataTable[ClassLayoutRow]):
     name = "ClassLayout"
     number = 15
 
@@ -730,27 +746,27 @@ class ClassLayout(ClrMetaDataTable):
 
 
 class FieldLayoutRowStruct(RowStruct):
-    Offset: int = None
-    Field_CodedIndex: int = None
+    Offset: int
+    Field_CodedIndex: int
 
 
 class FieldLayoutRow(MDTableRow):
     Offset: int
-    Field: FieldRow
+    Field: MDTableIndex[FieldRow]
 
-    struct: FieldLayoutRowStruct
     _struct_class = FieldLayoutRowStruct
 
     _struct_asis = {
         "Offset": "Offset",
     }
+    # TODO: should this be a codedindex?
     _struct_indexes = {
         "Field_CodedIndex": ("Field", "Field"),
     }
 
-    def _init_format(self):
+    def _compute_format(self):
         field_size = self._clr_coded_index_struct_size(0, ("Field",))
-        self._format = (
+        return (
             "CLR_METADATA_TABLE_FieldLayout",
             (
                 "I,Offset",
@@ -759,7 +775,7 @@ def _init_format(self):
         )
 
 
-class FieldLayout(ClrMetaDataTable):
+class FieldLayout(ClrMetaDataTable[FieldLayoutRow]):
     name = "FieldLayout"
     number = 16
 
@@ -771,28 +787,27 @@ class FieldLayout(ClrMetaDataTable):
 
 
 class StandAloneSigRowStruct(RowStruct):
-    Signature_BlobIndex: int = None
+    Signature_BlobIndex: int
 
 
 class StandAloneSigRow(MDTableRow):
-    Signature: bytes = None
+    Signature: bytes
 
-    struct: StandAloneSigRowStruct
     _struct_class = StandAloneSigRowStruct
 
     _struct_blobs = {
         "Signature_BlobIndex": "Signature",
     }
 
-    def _init_format(self):
-        blob_ind_size = utils.num_bytes_to_struct_char(self._blob_offsz)
-        self._format = (
+    def _compute_format(self):
+        blob_ind_size = checked_offset_format(self._blob_offsz)
+        return (
             "CLR_METADATA_TABLE_STANDALONESIG",
             (blob_ind_size + ",Signature_BlobIndex",),
         )
 
 
-class StandAloneSig(ClrMetaDataTable):
+class StandAloneSig(ClrMetaDataTable[StandAloneSigRow]):
     name = "StandAloneSig"
     number = 17
 
@@ -804,25 +819,24 @@ class StandAloneSig(ClrMetaDataTable):
 
 
 class EventMapRowStruct(RowStruct):
-    Parent_Index: int = None
-    EventList_Index: int = None
+    Parent_Index: int
+    EventList_Index: int
 
 
 class EventMapRow(MDTableRow):
-    Parent: TypeDefRow = None
-    EventList: List["EventRow"] = None
+    Parent: MDTableIndex[TypeDefRow]
+    EventList: List[MDTableIndex["EventRow"]]
 
-    struct: EventMapRowStruct
     _struct_class = EventMapRowStruct
 
     _struct_indexes = {
         "Parent_Index": ("Parent", "TypeDef"),
     }
 
-    def _init_format(self):
+    def _compute_format(self):
         parent_size = self._clr_coded_index_struct_size(0, ("TypeDef",))
         eventlist_size = self._clr_coded_index_struct_size(0, ("Event",))
-        self._format = (
+        return (
             "CLR_METADATA_TABLE_EVENTMAP",
             (
                 parent_size + ",Parent_Index",
@@ -831,7 +845,7 @@ def _init_format(self):
         )
 
 
-class EventMap(ClrMetaDataTable):
+class EventMap(ClrMetaDataTable[EventMapRow]):
     name = "EventMap"
     number = 18
 
@@ -853,17 +867,16 @@ class EventPtr(ClrMetaDataTable):
 
 
 class EventRowStruct(RowStruct):
-    EventFlags: int = None
-    Name_StringIndex: int = None
-    EventType_CodedIndex: int = None
+    EventFlags: int
+    Name_StringIndex: int
+    EventType_CodedIndex: int
 
 
 class EventRow(MDTableRow):
-    EventFlags: enums.ClrEventAttr = None
-    Name: str = None
-    EventType: codedindex.TypeDefOrRef = None
+    EventFlags: enums.ClrEventAttr
+    Name: str
+    EventType: codedindex.TypeDefOrRef
 
-    struct: EventRowStruct = None
     _struct_class = EventRowStruct
 
     _struct_flags = {
@@ -876,13 +889,13 @@ class EventRow(MDTableRow):
         "EventType_CodedIndex": ("EventType", codedindex.TypeDefOrRef),
     }
 
-    def _init_format(self):
-        str_ind_size = utils.num_bytes_to_struct_char(self._str_offsz)
+    def _compute_format(self):
+        str_ind_size = checked_offset_format(self._str_offsz)
         eventtype_size = self._clr_coded_index_struct_size(
             codedindex.TypeDefOrRef.tag_bits,
             codedindex.TypeDefOrRef.table_names,
         )
-        self._format = (
+        return (
             "CLR_METADATA_TABLE_EVENT",
             (
                 "H,EventFlags",
@@ -892,7 +905,7 @@ def _init_format(self):
         )
 
 
-class Event(ClrMetaDataTable):
+class Event(ClrMetaDataTable[EventRow]):
     name = "Event"
     number = 20
 
@@ -904,25 +917,24 @@ class Event(ClrMetaDataTable):
 
 
 class PropertyMapRowStruct(RowStruct):
-    Parent_Index: int = None
-    PropertyList_Index: int = None
+    Parent_Index: int
+    PropertyList_Index: int
 
 
 class PropertyMapRow(MDTableRow):
-    Parent: TypeDefRow = None
-    PropertyList: List["PropertyRow"] = None
+    Parent: MDTableIndex[TypeDefRow]
+    PropertyList: List[MDTableIndex["PropertyRow"]]
 
-    struct: PropertyMapRowStruct
     _struct_class = PropertyMapRowStruct
 
     _struct_indexes = {
         "Parent_Index": ("Parent", "TypeDef"),
     }
 
-    def _init_format(self):
+    def _compute_format(self):
         parent_size = self._clr_coded_index_struct_size(0, ("TypeDef",))
         propertylist_size = self._clr_coded_index_struct_size(0, ("Property",))
-        self._format = (
+        return (
             "CLR_METADATA_TABLE_PROPERTYMAP",
             (
                 parent_size + ",Parent_Index",
@@ -931,7 +943,7 @@ def _init_format(self):
         )
 
 
-class PropertyMap(ClrMetaDataTable):
+class PropertyMap(ClrMetaDataTable[PropertyMapRow]):
     name = "PropertyMap"
     number = 21
 
@@ -953,9 +965,9 @@ class PropertyPtr(ClrMetaDataTable):
 
 
 class PropertyRowStruct(RowStruct):
-    Flags: int = None
-    Name_StringIndex: int = None
-    Type_BlobIndex: int = None
+    Flags: int
+    Name_StringIndex: int
+    Type_BlobIndex: int
 
 
 class PropertyRow(MDTableRow):
@@ -963,7 +975,6 @@ class PropertyRow(MDTableRow):
     Name: str
     Type: bytes
 
-    struct: PropertyRowStruct
     _struct_class = PropertyRowStruct
 
     _struct_flags = {
@@ -976,10 +987,10 @@ class PropertyRow(MDTableRow):
         "Type_BlobIndex": "Type",
     }
 
-    def _init_format(self):
-        str_ind_size = utils.num_bytes_to_struct_char(self._str_offsz)
-        blob_ind_size = utils.num_bytes_to_struct_char(self._blob_offsz)
-        self._format = (
+    def _compute_format(self):
+        str_ind_size = checked_offset_format(self._str_offsz)
+        blob_ind_size = checked_offset_format(self._blob_offsz)
+        return (
             "CLR_METADATA_TABLE_PROPERTY",
             (
                 "H,Flags",
@@ -989,7 +1000,7 @@ def _init_format(self):
         )
 
 
-class Property(ClrMetaDataTable):
+class Property(ClrMetaDataTable[PropertyRow]):
     name = "Property"
     number = 23
 
@@ -1001,17 +1012,16 @@ class Property(ClrMetaDataTable):
 
 
 class MethodSemanticsRowStruct(RowStruct):
-    Semantics: int = None
-    Method_Index: int = None
-    Association_CodedIndex: int = None
+    Semantics: int
+    Method_Index: int
+    Association_CodedIndex: int
 
 
 class MethodSemanticsRow(MDTableRow):
     Semantics: enums.ClrMethodSemanticsAttr
-    Method: "MethodRow"
+    Method: MDTableIndex[MethodDefRow]
     Association: codedindex.HasSemantics
 
-    struct: MethodSemanticsRowStruct
     _struct_class = MethodSemanticsRowStruct
 
     _struct_flags = {
@@ -1025,13 +1035,13 @@ class MethodSemanticsRow(MDTableRow):
         "Association_CodedIndex": ("Association", codedindex.HasSemantics),
     }
 
-    def _init_format(self):
+    def _compute_format(self):
         method_size = self._clr_coded_index_struct_size(0, ("MethodDef",))
         association_size = self._clr_coded_index_struct_size(
             codedindex.HasSemantics.tag_bits,
             codedindex.HasSemantics.table_names,
         )
-        self._format = (
+        return (
             "CLR_METADATA_TABLE_METHODSEMANTICS",
             (
                 "H,Semantics",
@@ -1041,7 +1051,7 @@ def _init_format(self):
         )
 
 
-class MethodSemantics(ClrMetaDataTable):
+class MethodSemantics(ClrMetaDataTable[MethodSemanticsRow]):
     name = "MethodSemantics"
     number = 24
 
@@ -1053,17 +1063,16 @@ class MethodSemantics(ClrMetaDataTable):
 
 
 class MethodImplRowStruct(RowStruct):
-    Class_Index: int = None
-    MethodBody_CodedIndex: int = None
-    MethodDeclaration_CodedIndex: int = None
+    Class_Index: int
+    MethodBody_CodedIndex: int
+    MethodDeclaration_CodedIndex: int
 
 
 class MethodImplRow(MDTableRow):
-    Class: TypeDefRow
+    Class: MDTableIndex[TypeDefRow]
     MethodBody: codedindex.MethodDefOrRef
     MethodDeclaration: codedindex.MethodDefOrRef
 
-    struct: MethodImplRowStruct
     _struct_class = MethodImplRowStruct
 
     _struct_indexes = {
@@ -1077,13 +1086,13 @@ class MethodImplRow(MDTableRow):
         ),
     }
 
-    def _init_format(self):
+    def _compute_format(self):
         class_size = self._clr_coded_index_struct_size(0, ("TypeDef",))
         method_size = self._clr_coded_index_struct_size(
             codedindex.MethodDefOrRef.tag_bits,
             codedindex.MethodDefOrRef.table_names,
         )
-        self._format = (
+        return (
             "CLR_METADATA_TABLE_METHODIMPL",
             (
                 class_size + ",Class_Index",
@@ -1093,7 +1102,7 @@ def _init_format(self):
         )
 
 
-class MethodImpl(ClrMetaDataTable):
+class MethodImpl(ClrMetaDataTable[MethodImplRow]):
     name = "MethodImpl"
     number = 25
 
@@ -1105,28 +1114,27 @@ class MethodImpl(ClrMetaDataTable):
 
 
 class ModuleRefRowStruct(RowStruct):
-    Name_StringIndex: int = None
+    Name_StringIndex: int
 
 
 class ModuleRefRow(MDTableRow):
     Name: str
 
-    struct: ModuleRefRowStruct
     _struct_class = ModuleRefRowStruct
 
     _struct_strings = {
         "Name_StringIndex": "Name",
     }
 
-    def _init_format(self):
-        str_ind_size = utils.num_bytes_to_struct_char(self._str_offsz)
-        self._format = (
+    def _compute_format(self):
+        str_ind_size = checked_offset_format(self._str_offsz)
+        return (
             "CLR_METADATA_TABLE_MODULEREF",
             (str_ind_size + ",Name_StringIndex",),
         )
 
 
-class ModuleRef(ClrMetaDataTable):
+class ModuleRef(ClrMetaDataTable[ModuleRefRow]):
     name = "ModuleRef"
     number = 26
 
@@ -1138,28 +1146,27 @@ class ModuleRef(ClrMetaDataTable):
 
 
 class TypeSpecRowStruct(RowStruct):
-    Signature_BlobIndex: int = None
+    Signature_BlobIndex: int
 
 
 class TypeSpecRow(MDTableRow):
     Signature: bytes
 
-    struct: TypeSpecRowStruct
     _struct_class = TypeSpecRowStruct
 
     _struct_blobs = {
         "Signature_BlobIndex": "Signature",
     }
 
-    def _init_format(self):
-        blob_ind_size = utils.num_bytes_to_struct_char(self._blob_offsz)
-        self._format = (
+    def _compute_format(self):
+        blob_ind_size = checked_offset_format(self._blob_offsz)
+        return (
             "CLR_METADATA_TABLE_TYPESPEC",
             (blob_ind_size + ",Signature_BlobIndex",),
         )
 
 
-class TypeSpec(ClrMetaDataTable):
+class TypeSpec(ClrMetaDataTable[TypeSpecRow]):
     name = "TypeSpec"
     number = 27
 
@@ -1171,19 +1178,18 @@ class TypeSpec(ClrMetaDataTable):
 
 
 class ImplMapRowStruct(RowStruct):
-    MappingFlags: int = None
-    MemberForwarded_CodedIndex: int = None
-    ImportName_StringIndex: int = None
-    ImportScope_Index: int = None
+    MappingFlags: int
+    MemberForwarded_CodedIndex: int
+    ImportName_StringIndex: int
+    ImportScope_Index: int
 
 
 class ImplMapRow(MDTableRow):
     MappingFlags: enums.ClrPinvokeMap
     MemberForwarded: codedindex.MemberForwarded
     ImportName: str
-    ImportScope: ModuleRefRow
+    ImportScope: MDTableIndex[ModuleRefRow]
 
-    struct: ImplMapRowStruct
     _struct_class = ImplMapRowStruct
 
     _struct_flags = {
@@ -1199,14 +1205,14 @@ class ImplMapRow(MDTableRow):
         "ImportScope_Index": ("ImportScope", "ModuleRef"),
     }
 
-    def _init_format(self):
+    def _compute_format(self):
         member_size = self._clr_coded_index_struct_size(
             codedindex.MemberForwarded.tag_bits,
             codedindex.MemberForwarded.table_names,
         )
-        str_ind_size = utils.num_bytes_to_struct_char(self._str_offsz)
+        str_ind_size = checked_offset_format(self._str_offsz)
         importscope_size = self._clr_coded_index_struct_size(0, ("ModuleRef",))
-        self._format = (
+        return (
             "CLR_METADATA_TABLE_IMPLMAP",
             (
                 "H,MappingFlags",
@@ -1217,7 +1223,7 @@ def _init_format(self):
         )
 
 
-class ImplMap(ClrMetaDataTable):
+class ImplMap(ClrMetaDataTable[ImplMapRow]):
     name = "ImplMap"
     number = 28
 
@@ -1229,15 +1235,14 @@ class ImplMap(ClrMetaDataTable):
 
 
 class FieldRvaRowStruct(RowStruct):
-    Rva: int = None
-    Field_Index: int = None
+    Rva: int
+    Field_Index: int
 
 
 class FieldRvaRow(MDTableRow):
     Rva: int
-    Field: FieldRow
+    Field: MDTableIndex[FieldRow]
 
-    struct: FieldRvaRowStruct
     _struct_class = FieldRvaRowStruct
 
     _struct_asis = {
@@ -1247,9 +1252,9 @@ class FieldRvaRow(MDTableRow):
         "Field_Index": ("Field", "Field"),
     }
 
-    def _init_format(self):
+    def _compute_format(self):
         field_size = self._clr_coded_index_struct_size(0, ("Field",))
-        self._format = (
+        return (
             "CLR_METADATA_TABLE_FIELDRVA",
             (
                 "I,Rva",
@@ -1258,7 +1263,7 @@ def _init_format(self):
         )
 
 
-class FieldRva(ClrMetaDataTable):
+class FieldRva(ClrMetaDataTable[FieldRvaRow]):
     name = "FieldRva"
     number = 29
 
@@ -1280,15 +1285,15 @@ class Unused31(ClrMetaDataTable):
 
 
 class AssemblyRowStruct(RowStruct):
-    HashAlgId: int = None
-    MajorVersion: int = None
-    MinorVersion: int = None
-    BuildNumber: int = None
-    RevisionNumber: int = None
-    Flags: int = None
-    PublicKey_BlobIndex: int = None
-    Name_StringIndex: int = None
-    Culture_StringIndex: int = None
+    HashAlgId: int
+    MajorVersion: int
+    MinorVersion: int
+    BuildNumber: int
+    RevisionNumber: int
+    Flags: int
+    PublicKey_BlobIndex: int
+    Name_StringIndex: int
+    Culture_StringIndex: int
 
 
 class AssemblyRow(MDTableRow):
@@ -1302,13 +1307,14 @@ class AssemblyRow(MDTableRow):
     Name: str
     Culture: str
 
-    struct: AssemblyRowStruct
     _struct_class = AssemblyRowStruct
 
     _struct_flags = {
-        "HashAlgId": ("HashAlgId", enums.AssemblyHashAlgorithm),
         "Flags": ("Flags", enums.ClrAssemblyFlags),
     }
+    _struct_enums = {
+        "HashAlgId": ("HashAlgId", enums.AssemblyHashAlgorithm),
+    }
     _struct_asis = {
         "MajorVersion": "MajorVersion",
         "MinorVersion": "MinorVersion",
@@ -1323,10 +1329,10 @@ class AssemblyRow(MDTableRow):
         "Culture_StringIndex": "Culture",
     }
 
-    def _init_format(self):
-        str_ind_size = utils.num_bytes_to_struct_char(self._str_offsz)
-        blob_ind_size = utils.num_bytes_to_struct_char(self._blob_offsz)
-        self._format = (
+    def _compute_format(self):
+        str_ind_size = checked_offset_format(self._str_offsz)
+        blob_ind_size = checked_offset_format(self._blob_offsz)
+        return (
             "CLR_METADATA_TABLE_ASSEMBLY",
             (
                 "I,HashAlgId",
@@ -1342,7 +1348,7 @@ def _init_format(self):
         )
 
 
-class Assembly(ClrMetaDataTable):
+class Assembly(ClrMetaDataTable[AssemblyRow]):
     name = "Assembly"
     number = 32
 
@@ -1354,13 +1360,12 @@ class Assembly(ClrMetaDataTable):
 
 
 class AssemblyProcessorRowStruct(RowStruct):
-    Processor: int = None
+    Processor: int
 
 
 class AssemblyProcessorRow(MDTableRow):
     Processor: int
 
-    struct: AssemblyProcessorRowStruct
     _struct_class = AssemblyProcessorRowStruct
 
     _format = ("CLR_METADATA_TABLE_ASSEMBLYPROCESSOR", ("I,Processor",))
@@ -1370,7 +1375,7 @@ class AssemblyProcessorRow(MDTableRow):
     }
 
 
-class AssemblyProcessor(ClrMetaDataTable):
+class AssemblyProcessor(ClrMetaDataTable[AssemblyProcessorRow]):
     name = "AssemblyProcessor"
     number = 33
 
@@ -1382,9 +1387,9 @@ class AssemblyProcessor(ClrMetaDataTable):
 
 
 class AssemblyOSRowStruct(RowStruct):
-    OSPlatformID: int = None
-    OSMajorVersion: int = None
-    OSMinorVersion: int = None
+    OSPlatformID: int
+    OSMajorVersion: int
+    OSMinorVersion: int
 
 
 class AssemblyOSRow(MDTableRow):
@@ -1392,7 +1397,6 @@ class AssemblyOSRow(MDTableRow):
     OSMajorVersion: int
     OSMinorVersion: int
 
-    struct: AssemblyOSRowStruct
     _struct_class = AssemblyOSRowStruct
 
     _format = (
@@ -1411,7 +1415,7 @@ class AssemblyOSRow(MDTableRow):
     }
 
 
-class AssemblyOS(ClrMetaDataTable):
+class AssemblyOS(ClrMetaDataTable[AssemblyOSRow]):
     name = "AssemblyOS"
     number = 34
 
@@ -1423,15 +1427,15 @@ class AssemblyOS(ClrMetaDataTable):
 
 
 class AssemblyRefRowStruct(RowStruct):
-    MajorVersion: int = None
-    MinorVersion: int = None
-    BuildNumber: int = None
-    RevisionNumber: int = None
-    Flags: int = None
-    PublicKey_BlobIndex: int = None
-    Name_StringIndex: int = None
-    Culture_StringIndex: int = None
-    HashValue_BlobIndex: int = None
+    MajorVersion: int
+    MinorVersion: int
+    BuildNumber: int
+    RevisionNumber: int
+    Flags: int
+    PublicKey_BlobIndex: int
+    Name_StringIndex: int
+    Culture_StringIndex: int
+    HashValue_BlobIndex: int
 
 
 class AssemblyRefRow(MDTableRow):
@@ -1445,7 +1449,6 @@ class AssemblyRefRow(MDTableRow):
     Culture: str
     HashValue: bytes
 
-    struct: AssemblyRefRowStruct
     _struct_class = AssemblyRefRowStruct
 
     _struct_asis = {
@@ -1466,10 +1469,10 @@ class AssemblyRefRow(MDTableRow):
         "Culture_StringIndex": "Culture",
     }
 
-    def _init_format(self):
-        str_ind_size = utils.num_bytes_to_struct_char(self._str_offsz)
-        blob_ind_size = utils.num_bytes_to_struct_char(self._blob_offsz)
-        self._format = (
+    def _compute_format(self):
+        str_ind_size = checked_offset_format(self._str_offsz)
+        blob_ind_size = checked_offset_format(self._blob_offsz)
+        return (
             "CLR_METADATA_TABLE_ASSEMBLYREF",
             (
                 "H,MajorVersion",
@@ -1485,7 +1488,7 @@ def _init_format(self):
         )
 
 
-class AssemblyRef(ClrMetaDataTable):
+class AssemblyRef(ClrMetaDataTable[AssemblyRefRow]):
     name = "AssemblyRef"
     number = 35
 
@@ -1497,15 +1500,14 @@ class AssemblyRef(ClrMetaDataTable):
 
 
 class AssemblyRefProcessorRowStruct(RowStruct):
-    Processor: int = None
-    AssemblyRef_Index: int = None
+    Processor: int
+    AssemblyRef_Index: int
 
 
 class AssemblyRefProcessorRow(MDTableRow):
     Processor: int
-    AssemblyRef: AssemblyRefRow
+    AssemblyRef: MDTableIndex[AssemblyRefRow]
 
-    struct: AssemblyRefProcessorRowStruct
     _struct_class = AssemblyRefProcessorRowStruct
 
     _struct_asis = {
@@ -1515,9 +1517,9 @@ class AssemblyRefProcessorRow(MDTableRow):
         "AssemblyRef_Index": ("AssemblyRef", "AssemblyRef"),
     }
 
-    def _init_format(self):
+    def _compute_format(self):
         assemblyref_size = self._clr_coded_index_struct_size(0, ("AssemblyRef",))
-        self._format = (
+        return (
             "CLR_METADATA_TABLE_ASSEMBLYREFPROCESSOR",
             (
                 "I,Processor",
@@ -1526,7 +1528,7 @@ def _init_format(self):
         )
 
 
-class AssemblyRefProcessor(ClrMetaDataTable):
+class AssemblyRefProcessor(ClrMetaDataTable[AssemblyRefProcessorRow]):
     name = "AssemblyRefProcessor"
     number = 36
 
@@ -1538,19 +1540,18 @@ class AssemblyRefProcessor(ClrMetaDataTable):
 
 
 class AssemblyRefOSRowStruct(RowStruct):
-    OSPlatformId: int = None
-    OSMajorVersion: int = None
-    OSMinorVersion: int = None
-    AssemblyRef_Index: int = None
+    OSPlatformId: int
+    OSMajorVersion: int
+    OSMinorVersion: int
+    AssemblyRef_Index: int
 
 
 class AssemblyRefOSRow(MDTableRow):
     OSPlatformId: int
     OSMajorVersion: int
     OSMinorVersion: int
-    AssemblyRef: AssemblyRefRow
+    AssemblyRef: MDTableIndex[AssemblyRefRow]
 
-    struct: AssemblyRefOSRowStruct
     _struct_class = AssemblyRefOSRowStruct
 
     _struct_asis = {
@@ -1562,9 +1563,9 @@ class AssemblyRefOSRow(MDTableRow):
         "AssemblyRef_Index": ("AssemblyRef", "AssemblyRef"),
     }
 
-    def _init_format(self):
+    def _compute_format(self):
         assemblyref_size = self._clr_coded_index_struct_size(0, ("AssemblyRef",))
-        self._format = (
+        return (
             "CLR_METADATA_TABLE_ASSEMBLYREFOS",
             (
                 "I,OSPlatformId",
@@ -1575,7 +1576,7 @@ def _init_format(self):
         )
 
 
-class AssemblyRefOS(ClrMetaDataTable):
+class AssemblyRefOS(ClrMetaDataTable[AssemblyRefOSRow]):
     name = "AssemblyRefOS"
     number = 37
 
@@ -1587,9 +1588,9 @@ class AssemblyRefOS(ClrMetaDataTable):
 
 
 class FileRowStruct(RowStruct):
-    Flags: int = None
-    Name_StringIndex: int = None
-    HashValue_BlobIndex: int = None
+    Flags: int
+    Name_StringIndex: int
+    HashValue_BlobIndex: int
 
 
 class FileRow(MDTableRow):
@@ -1597,7 +1598,6 @@ class FileRow(MDTableRow):
     Name: str
     HashValue: bytes
 
-    struct: FileRowStruct
     _struct_class = FileRowStruct
 
     _struct_flags = {
@@ -1610,10 +1610,10 @@ class FileRow(MDTableRow):
         "HashValue_BlobIndex": "HashValue",
     }
 
-    def _init_format(self):
-        str_ind_size = utils.num_bytes_to_struct_char(self._str_offsz)
-        blob_ind_size = utils.num_bytes_to_struct_char(self._blob_offsz)
-        self._format = (
+    def _compute_format(self):
+        str_ind_size = checked_offset_format(self._str_offsz)
+        blob_ind_size = checked_offset_format(self._blob_offsz)
+        return (
             "CLR_METADATA_TABLE_FILE",
             (
                 "I,Flags",
@@ -1623,7 +1623,7 @@ def _init_format(self):
         )
 
 
-class File(ClrMetaDataTable):
+class File(ClrMetaDataTable[FileRow]):
     name = "File"
     number = 38
 
@@ -1635,11 +1635,11 @@ class File(ClrMetaDataTable):
 
 
 class ExportedTypeRowStruct(RowStruct):
-    Flags: int = None
-    TypeDefId: int = None
-    TypeName_StringIndex: int = None
-    TypeNamespace_BlobIndex: int = None
-    Implementation_CodedIndex: int = None
+    Flags: int
+    TypeDefId: int
+    TypeName_StringIndex: int
+    TypeNamespace_BlobIndex: int
+    Implementation_CodedIndex: int
 
 
 class ExportedTypeRow(MDTableRow):
@@ -1649,7 +1649,6 @@ class ExportedTypeRow(MDTableRow):
     TypeNamespace: bytes
     Implementation: codedindex.Implementation
 
-    struct: ExportedTypeRowStruct
     _struct_class = ExportedTypeRowStruct
 
     _struct_flags = {
@@ -1668,14 +1667,14 @@ class ExportedTypeRow(MDTableRow):
         "Implementation_CodedIndex": ("Implementation", codedindex.Implementation),
     }
 
-    def _init_format(self):
-        str_ind_size = utils.num_bytes_to_struct_char(self._str_offsz)
-        blob_ind_size = utils.num_bytes_to_struct_char(self._blob_offsz)
+    def _compute_format(self):
+        str_ind_size = checked_offset_format(self._str_offsz)
+        blob_ind_size = checked_offset_format(self._blob_offsz)
         implementation_size = self._clr_coded_index_struct_size(
             codedindex.Implementation.tag_bits,
             codedindex.Implementation.table_names,
         )
-        self._format = (
+        return (
             "CLR_METADATA_TABLE_EXPORTEDTYPE",
             (
                 "I,Flags",
@@ -1687,7 +1686,7 @@ def _init_format(self):
         )
 
 
-class ExportedType(ClrMetaDataTable):
+class ExportedType(ClrMetaDataTable[ExportedTypeRow]):
     name = "ExportedType"
     number = 39
 
@@ -1699,10 +1698,10 @@ class ExportedType(ClrMetaDataTable):
 
 
 class ManifestResourceRowStruct(RowStruct):
-    Offset: int = None
-    Flags: int = None
-    Name_StringIndex: int = None
-    Implementation_CodedIndex: int = None
+    Offset: int
+    Flags: int
+    Name_StringIndex: int
+    Implementation_CodedIndex: int
 
 
 class ManifestResourceRow(MDTableRow):
@@ -1711,7 +1710,6 @@ class ManifestResourceRow(MDTableRow):
     Name: str
     Implementation: codedindex.Implementation
 
-    struct: ManifestResourceRowStruct
     _struct_class = ManifestResourceRowStruct
 
     _struct_asis = {
@@ -1727,13 +1725,13 @@ class ManifestResourceRow(MDTableRow):
         "Implementation_CodedIndex": ("Implementation", codedindex.Implementation),
     }
 
-    def _init_format(self):
-        str_ind_size = utils.num_bytes_to_struct_char(self._str_offsz)
+    def _compute_format(self):
+        str_ind_size = checked_offset_format(self._str_offsz)
         implementation_size = self._clr_coded_index_struct_size(
             codedindex.Implementation.tag_bits,
             codedindex.Implementation.table_names,
         )
-        self._format = (
+        return (
             "CLR_METADATA_TABLE_MANIFESTRESOURCE",
             (
                 "I,Offset",
@@ -1744,7 +1742,7 @@ def _init_format(self):
         )
 
 
-class ManifestResource(ClrMetaDataTable):
+class ManifestResource(ClrMetaDataTable[ManifestResourceRow]):
     name = "ManifestResource"
     number = 40
 
@@ -1756,15 +1754,14 @@ class ManifestResource(ClrMetaDataTable):
 
 
 class NestedClassRowStruct(RowStruct):
-    NestedClass_Index: int = None
-    EnclosingClass_Index: int = None
+    NestedClass_Index: int
+    EnclosingClass_Index: int
 
 
 class NestedClassRow(MDTableRow):
-    NestedClass: TypeDefRow
-    EnclosingClass: TypeDefRow
+    NestedClass: MDTableIndex[TypeDefRow]
+    EnclosingClass: MDTableIndex[TypeDefRow]
 
-    struct: NestedClassRowStruct
     _struct_class = NestedClassRowStruct
 
     _struct_indexes = {
@@ -1772,9 +1769,9 @@ class NestedClassRow(MDTableRow):
         "EnclosingClass_Index": ("EnclosingClass", "TypeDef"),
     }
 
-    def _init_format(self):
+    def _compute_format(self):
         class_size = self._clr_coded_index_struct_size(0, ("TypeDef",))
-        self._format = (
+        return (
             "CLR_METADATA_TABLE_NESTEDCLASS",
             (
                 class_size + ",NestedClass_Index",
@@ -1783,7 +1780,7 @@ def _init_format(self):
         )
 
 
-class NestedClass(ClrMetaDataTable):
+class NestedClass(ClrMetaDataTable[NestedClassRow]):
     name = "NestedClass"
     number = 41
 
@@ -1795,10 +1792,10 @@ class NestedClass(ClrMetaDataTable):
 
 
 class GenericParamRowStruct(RowStruct):
-    Number: int = None
-    Flags: int = None
-    Owner_CodedIndex: int = None
-    Name_StringIndex: int = None
+    Number: int
+    Flags: int
+    Owner_CodedIndex: int
+    Name_StringIndex: int
 
 
 class GenericParamRow(MDTableRow):
@@ -1807,7 +1804,6 @@ class GenericParamRow(MDTableRow):
     Owner: codedindex.TypeOrMethodDef
     Name: str
 
-    struct: GenericParamRowStruct
     _struct_class = GenericParamRowStruct
 
     _struct_asis = {
@@ -1823,13 +1819,13 @@ class GenericParamRow(MDTableRow):
         "Name_StringIndex": "Name",
     }
 
-    def _init_format(self):
+    def _compute_format(self):
         owner_size = self._clr_coded_index_struct_size(
             codedindex.TypeOrMethodDef.tag_bits,
             codedindex.TypeOrMethodDef.table_names,
         )
-        str_ind_size = utils.num_bytes_to_struct_char(self._str_offsz)
-        self._format = (
+        str_ind_size = checked_offset_format(self._str_offsz)
+        return (
             "CLR_METADATA_TABLE_GENERICPARAM",
             (
                 "H,Number",
@@ -1840,7 +1836,7 @@ def _init_format(self):
         )
 
 
-class GenericParam(ClrMetaDataTable):
+class GenericParam(ClrMetaDataTable[GenericParamRow]):
     name = "GenericParam"
     number = 42
 
@@ -1852,15 +1848,14 @@ class GenericParam(ClrMetaDataTable):
 
 
 class GenericMethodRowStruct(RowStruct):
-    Unknown1_CodedIndex: int = None
-    Unknown2_BlobIndex: int = None
+    Unknown1_CodedIndex: int
+    Unknown2_BlobIndex: int
 
 
 class GenericMethodRow(MDTableRow):
     Unknown1: codedindex.MethodDefOrRef
     Unknown2: bytes
 
-    struct: GenericMethodRowStruct
     _struct_class = GenericMethodRowStruct
 
     _struct_codedindexes = {
@@ -1870,13 +1865,13 @@ class GenericMethodRow(MDTableRow):
         "Unknown2_BlobIndex": "Unknown2",
     }
 
-    def _init_format(self):
+    def _compute_format(self):
         unknown1_size = self._clr_coded_index_struct_size(
             codedindex.MethodDefOrRef.tag_bits,
             codedindex.MethodDefOrRef.table_names,
         )
-        blob_ind_size = utils.num_bytes_to_struct_char(self._blob_offsz)
-        self._format = (
+        blob_ind_size = checked_offset_format(self._blob_offsz)
+        return (
             "CLR_METADATA_TABLE_GENERICMETHOD",
             (
                 unknown1_size + ",Unknown1_CodedIndex",
@@ -1885,7 +1880,7 @@ def _init_format(self):
         )
 
 
-class GenericMethod(ClrMetaDataTable):
+class GenericMethod(ClrMetaDataTable[GenericMethodRow]):
     name = "GenericMethod"
     number = 43
 
@@ -1897,15 +1892,14 @@ class GenericMethod(ClrMetaDataTable):
 
 
 class GenericParamConstraintRowStruct(RowStruct):
-    Owner_Index: int = None
-    Constraint_CodedIndex: int = None
+    Owner_Index: int
+    Constraint_CodedIndex: int
 
 
 class GenericParamConstraintRow(MDTableRow):
-    Owner: GenericParamRow
+    Owner: MDTableIndex[GenericParamRow]
     Constraint: codedindex.TypeDefOrRef
 
-    struct: GenericParamConstraintRowStruct
     _struct_class = GenericParamConstraintRowStruct
 
     _struct_indexes = {
@@ -1915,13 +1909,13 @@ class GenericParamConstraintRow(MDTableRow):
         "Constraint_CodedIndex": ("Constraint", codedindex.TypeDefOrRef),
     }
 
-    def _init_format(self):
+    def _compute_format(self):
         owner_size = self._clr_coded_index_struct_size(0, ("GenericParam",))
         constraint_size = self._clr_coded_index_struct_size(
             codedindex.TypeDefOrRef.tag_bits,
             codedindex.TypeDefOrRef.table_names,
         )
-        self._format = (
+        return (
             "CLR_METADATA_TABLE_GENERICPARAMCONSTRAINT",
             (
                 owner_size + ",Owner_Index",
@@ -1930,7 +1924,7 @@ def _init_format(self):
         )
 
 
-class GenericParamConstraint(ClrMetaDataTable):
+class GenericParamConstraint(ClrMetaDataTable[GenericParamConstraintRow]):
     name = "GenericParamConstraint"
     number = 44
 
@@ -1946,16 +1940,20 @@ class GenericParamConstraint(ClrMetaDataTable):
 # 61
 # 62
 
+class Unused(ClrMetaDataTable):
+    # placeholder for unused table references.
+    # which is referenced by `CustomAttributeType` coded index.
+    name = "Unused"
+    number = 62
+
 
 class MaxTable(ClrMetaDataTable):
     name = "MaxTable"
     number = 63
 
-    # TODO
-
 
 class ClrMetaDataTableFactory(object):
-    _table_number_map = {
+    _table_number_map: Dict[int, Type[ClrMetaDataTable]] = {
         0: Module,
         1: TypeRef,
         2: TypeDef,
@@ -2002,6 +2000,7 @@ class ClrMetaDataTableFactory(object):
         43: GenericMethod,
         44: GenericParamConstraint,
         # 45 through 63 are not used
+        62: Unused,
         63: MaxTable,
     }
 
@@ -2009,26 +2008,26 @@ class ClrMetaDataTableFactory(object):
     def createTable(
         cls,
         number: int,
-        tables_rowcounts: List[int],
+        tables_rowcounts: List[Optional[int]],
         is_sorted: bool,
         strings_offset_size: int,
         guid_offset_size: int,
         blob_offset_size: int,
-        strings_heap: ClrHeap,
-        guid_heap: ClrHeap,
-        blob_heap: ClrHeap,
+        strings_heap: Optional["stream.StringsHeap"],
+        guid_heap: Optional["stream.GuidHeap"],
+        blob_heap: Optional["stream.BlobHeap"],
     ) -> ClrMetaDataTable:
-        if number in cls._table_number_map:
-            table = cls._table_number_map[number](
-                tables_rowcounts,
-                is_sorted,
-                strings_offset_size,
-                guid_offset_size,
-                blob_offset_size,
-                strings_heap,
-                guid_heap,
-                blob_heap,
-            )
-            return table
-        else:
-            return None
+        if number not in cls._table_number_map:
+            raise errors.dnFormatError("invalid table index")
+
+        table = cls._table_number_map[number](
+            tables_rowcounts,
+            is_sorted,
+            strings_offset_size,
+            guid_offset_size,
+            blob_offset_size,
+            strings_heap,
+            guid_heap,
+            blob_heap,
+        )
+        return table
diff --git a/src/dnfile/stream.py b/src/dnfile/stream.py
index 58721ff..eeb2ef2 100644
--- a/src/dnfile/stream.py
+++ b/src/dnfile/stream.py
@@ -10,16 +10,18 @@
 Copyright (c) 2020-2021 MalwareFrank
 """
 
-import copy as _copymod
 import struct as _struct
-from typing import Dict, List, Tuple
+import logging
+from typing import Dict, List, Tuple, Union, Optional
 from binascii import hexlify as _hexlify
 
-from pefile import MAX_STRING_LENGTH, Structure, DataContainer
+from pefile import MAX_STRING_LENGTH, Structure
 
 from . import base, errors, mdtable
 from .utils import read_compressed_int
 
+logger = logging.getLogger(__name__)
+
 
 class GenericStream(base.ClrStream):
     """
@@ -55,22 +57,44 @@ def get(self, index, max_length=MAX_STRING_LENGTH, encoding="utf-8", as_bytes=Fa
 
 
 class BinaryHeap(base.ClrHeap):
-    def get_with_size(self, index) -> Tuple[bytes, int]:
+    def get_with_size(self, index) -> Optional[Tuple[bytes, int]]:
+        if self.__data__ is None:
+            logger.warning("stream has no data")
+            return None
+
         if index >= len(self.__data__):
-            raise IndexError("index out of range")
+            logger.warning("stream is too small: wanted: 0x%x found: 0x%x", index, len(self.__data__))
+            return None
 
         offset = index
+
         # read compressed int length
-        data_length, length_size = read_compressed_int(
-            self.__data__[offset:offset + 4]
-        )
+        buf = self.__data__[offset:offset + 4]
+        ret = read_compressed_int(buf)
+        if ret is None:
+            # invalid compressed int length, such as invalid leading flags.
+            logger.warning("stream entry has invalid compressed int")
+            return None
+
+        data_length, length_size = ret
+
         # read data
         offset = offset + length_size
         data = self.__data__[offset:offset + data_length]
+
         return data, length_size + data_length
 
-    def get(self, index) -> bytes:
-        data, _ = self.get_with_size(index)
+    def get(self, index) -> Optional[bytes]:
+        try:
+            ret = self.get_with_size(index)
+        except IndexError:
+            return None
+
+        if ret is None:
+            return None
+
+        data, _ = ret
+
         return data
 
 
@@ -87,26 +111,26 @@ class UserString(object):
 
     Reference ECMA-335, Partition II Section 24.2.4
     """
-    value: str = None
-    Flag: int = None
-
-    __data__: bytes = None
-
     def __init__(self, data: bytes, encoding="utf-16"):
-        self.__data__ = data
+        self.__data__: bytes = data
+
+        self.Flag: int = 0
         if len(data) % 2 == 1:
             self.Flag = data[-1]
             data = data[:-1]
         else:
-            # TODO error/warn
-            pass
-        self.value = data.decode(encoding)
+            logger.warning("string missing trailing flag")
+
+        self.value: str = data.decode(encoding)
 
 
 class UserStringHeap(BinaryHeap):
-    def get_us(self, index, max_length=MAX_STRING_LENGTH, encoding="utf-16") -> UserString:
+    def get_us(self, index, max_length=MAX_STRING_LENGTH, encoding="utf-16") -> Optional[UserString]:
         data = self.get(index)
-        return UserString(data)
+        if data is None:
+            return None
+        else:
+            return UserString(data)
 
 
 class GuidHeap(base.ClrHeap):
@@ -173,7 +197,7 @@ class MetaDataTables(base.ClrStream):
     )
 
     header: MDTablesStruct
-    tables: Dict[str, base.ClrMetaDataTable]
+    tables: Dict[Union[str, int], base.ClrMetaDataTable]
     tables_list: List[base.ClrMetaDataTable]
     strings_offset_size: int
     guids_offset_size: int
@@ -181,46 +205,50 @@ class MetaDataTables(base.ClrStream):
 
     # from https://www.ntcore.com/files/dotnetformat.htm
     # and https://referencesource.microsoft.com/System.AddIn/System/Addin/MiniReflection/MetadataReader/Metadata.cs.html#123
-    Module:                 mdtable.Module = None
-    TypeRef:                mdtable.TypeRef = None
-    TypeDef:                mdtable.TypeDef = None
-    Field:                  mdtable.Field = None
-    MethodDef:              mdtable.MethodDef = None
-    Param:                  mdtable.Param = None
-    InterfaceImpl:          mdtable.InterfaceImpl = None
-    MemberRef:              mdtable.MemberRef = None
-    Constant:               mdtable.Constant = None
-    CustomAttribute:        mdtable.CustomAttribute = None
-    FieldMarshal:           mdtable.FieldMarshal = None
-    DeclSecurity:           mdtable.DeclSecurity = None
-    ClassLayout:            mdtable.ClassLayout = None
-    FieldLayout:            mdtable.FieldLayout = None
-    StandAloneSig:          mdtable.StandAloneSig = None
-    EventMap:               mdtable.EventMap = None
-    Event:                  mdtable.Event = None
-    PropertyMap:            mdtable.PropertyMap = None
-    Property:               mdtable.Property = None
-    MethodSemantics:        mdtable.MethodSemantics = None
-    MethodImpl:             mdtable.MethodImpl = None
-    ModuleRef:              mdtable.ModuleRef = None
-    TypeSpec:               mdtable.TypeSpec = None
-    ImplMap:                mdtable.ImplMap = None
-    FieldRva:               mdtable.FieldRva = None
-    Assembly:               mdtable.Assembly = None
-    AssemblyProcessor:      mdtable.AssemblyProcessor = None
-    AssemblyOS:             mdtable.AssemblyOS = None
-    AssemblyRef:            mdtable.AssemblyRef = None
-    AssemblyRefProcessor:   mdtable.AssemblyRefProcessor = None
-    AssemblyRefOS:          mdtable.AssemblyRefOS = None
-    File:                   mdtable.File = None
-    ExportedType:           mdtable.ExportedType = None
-    ManifestResource:       mdtable.ManifestResource = None
-    NestedClass:            mdtable.NestedClass = None
-    GenericParam:           mdtable.GenericParam = None
-    GenericParamConstraint: mdtable.GenericParamConstraint = None
+    Module:                 mdtable.Module
+    TypeRef:                mdtable.TypeRef
+    TypeDef:                mdtable.TypeDef
+    Field:                  mdtable.Field
+    MethodDef:              mdtable.MethodDef
+    Param:                  mdtable.Param
+    InterfaceImpl:          mdtable.InterfaceImpl
+    MemberRef:              mdtable.MemberRef
+    Constant:               mdtable.Constant
+    CustomAttribute:        mdtable.CustomAttribute
+    FieldMarshal:           mdtable.FieldMarshal
+    DeclSecurity:           mdtable.DeclSecurity
+    ClassLayout:            mdtable.ClassLayout
+    FieldLayout:            mdtable.FieldLayout
+    StandAloneSig:          mdtable.StandAloneSig
+    EventMap:               mdtable.EventMap
+    Event:                  mdtable.Event
+    PropertyMap:            mdtable.PropertyMap
+    Property:               mdtable.Property
+    MethodSemantics:        mdtable.MethodSemantics
+    MethodImpl:             mdtable.MethodImpl
+    ModuleRef:              mdtable.ModuleRef
+    TypeSpec:               mdtable.TypeSpec
+    ImplMap:                mdtable.ImplMap
+    FieldRva:               mdtable.FieldRva
+    Assembly:               mdtable.Assembly
+    AssemblyProcessor:      mdtable.AssemblyProcessor
+    AssemblyOS:             mdtable.AssemblyOS
+    AssemblyRef:            mdtable.AssemblyRef
+    AssemblyRefProcessor:   mdtable.AssemblyRefProcessor
+    AssemblyRefOS:          mdtable.AssemblyRefOS
+    File:                   mdtable.File
+    ExportedType:           mdtable.ExportedType
+    ManifestResource:       mdtable.ManifestResource
+    NestedClass:            mdtable.NestedClass
+    GenericParam:           mdtable.GenericParam
+    GenericParamConstraint: mdtable.GenericParamConstraint
+    Unused:                 mdtable.Unused
 
     def parse(self, streams: List[base.ClrStream]):
-
+        """
+        this may raise an exception if the data cannot be parsed correctly.
+        however, `self` may still be partially initialized with *some* data.
+        """
         STRINGS_MASK = 0x01
         GUIDS_MASK = 0x02
         BLOBS_MASK = 0x04
@@ -229,14 +257,17 @@ def parse(self, streams: List[base.ClrStream]):
         HAS_DELETE_MASK = 0x80
         MAX_TABLES = 64
 
-        warnings = list()
+        # we may be able to parse some data before reaching corruption.
+        # so, we'll keep parsing all we can, which updates `self` in-place,
+        # and then raise the first deferred exception captured here.
+        deferred_exceptions = list()
 
         self.tables = dict()
         self.tables_list = list()
         header_len = Structure(self._format).sizeof()
         if not self.__data__ or len(self.__data__) < header_len:
-            # warning
-            raise errors.dnFileFormat("Unable to read .NET metadata tables")
+            logger.warning("unable to read .NET metadata tables")
+            raise errors.dnFormatError("Unable to read .NET metadata tables")
 
         #### parse header
         header_struct = MDTablesStruct(self._format, file_offset=self.rva)
@@ -261,9 +292,9 @@ def parse(self, streams: List[base.ClrStream]):
         self.blobs_offset_size = blobs_offset_size
 
         #### heaps
-        strings_heap: StringsHeap = None
-        guid_heap: GuidHeap = None
-        blob_heap: BlobHeap = None
+        strings_heap: Optional[StringsHeap] = None
+        guid_heap: Optional[GuidHeap] = None
+        blob_heap: Optional[BlobHeap] = None
         for s in streams:
             # find the first instance of the strings, guid, and blob heaps
             # TODO: if there are multiple instances of a type, does dotnet runtime use first?
@@ -281,16 +312,17 @@ def parse(self, streams: List[base.ClrStream]):
         #  are listed, thus the variable length and need to parse
         #  the header's MaskValid member.
         cur_rva = self.rva + header_len
-        # initialize table with zero row counts for all tables
-        table_rowcounts = [0] * MAX_TABLES
+        table_rowcounts = []
         # read all row counts
         for i in range(MAX_TABLES):
             # if table bit is set
             if header_struct.MaskValid & 2 ** i != 0:
                 # read the row count
-                table_rowcounts[i] = self.get_dword_at_rva(cur_rva)
+                table_rowcounts.append(self.get_dword_at_rva(cur_rva))
                 # increment to next dword
                 cur_rva += 4
+            else:
+                table_rowcounts.append(0)
 
         # consume an extra dword if the extra data bit is set
         if header_struct.HeapOffsetSizes & EXTRA_DATA_MASK == EXTRA_DATA_MASK:
@@ -313,14 +345,15 @@ def parse(self, streams: List[base.ClrStream]):
                     blob_heap,
                 )
                 if not table:
-                    # delay error/warning
-                    warnings.append(
+                    logger.warning("invalid .NET metadata table list @ %d RVA: 0x%x", i, cur_rva)
+                    # Everything up to this point has been saved in the object and is accessible,
+                    # but more can be parsed, so we delay raising exception.
+
+                    deferred_exceptions.append(errors.dnFormatError(
                         "Invalid .NET metadata table list @ {} rva:{}".format(
                             i, cur_rva
-                        )
+                        ))
                     )
-                    # Everything up to this point has been saved in the object and is accessible,
-                    # but more can be parsed, so we delay raising exception.
                 # table number
                 table.number = i
                 # add to tables dict
@@ -339,8 +372,7 @@ def parse(self, streams: List[base.ClrStream]):
                     cur_rva, table.row_size * table.num_rows
                 )
                 # parse structures (populates .struct for each row)
-                table.parse_rows(table_data)
-                table.rva = cur_rva
+                table.parse_rows(cur_rva, table_data)
                 # move to next set of rows
                 cur_rva += table.row_size * table.num_rows
         #### finalize parsing each table
@@ -349,5 +381,5 @@ def parse(self, streams: List[base.ClrStream]):
             table.parse(self.tables_list)
 
         # raise warning/error
-        if warnings:
-            raise errors.dnFormatError(warnings[0])
+        if deferred_exceptions:
+            raise deferred_exceptions[0]
diff --git a/src/dnfile/utils.py b/src/dnfile/utils.py
index d12dd15..85bda25 100644
--- a/src/dnfile/utils.py
+++ b/src/dnfile/utils.py
@@ -1,9 +1,11 @@
 # -*- coding: utf-8 -*-
 
 import copy as _copymod
+import logging
 import functools as _functools
+from typing import Tuple, Optional
 
-from pefile import Structure
+logger = logging.getLogger(__name__)
 
 
 # lru_cache with a shallow copy of the objects returned (list, dicts, ..)
@@ -26,14 +28,13 @@ def wrapper(*args, **kwargs):
     return decorator
 
 
-def read_compressed_int(data):
+def read_compressed_int(data) -> Optional[Tuple[int, int]]:
     """
     Given bytes, read a compressed integer per
     spec ECMA-335 II.23.2 Blobs and signatures.
-    Returns tuple: value, number of bytes read
+    Returns tuple: value, number of bytes read or None on error.
     """
     if not data:
-        # TODO: error
         return None
     if data[0] & 0x80 == 0:
         # values 0x00 to 0x7f
@@ -50,29 +51,35 @@ def read_compressed_int(data):
         value |= data[2] << 8
         value |= data[3]
         return value, 4
-    # TODO: error
+    else:
+        logger.warning("invalid compressed int: leading byte: 0x%02x", data[0])
+        return None
 
 
 def two_way_dict(pairs):
     return dict([(e[1], e[0]) for e in pairs] + pairs)
 
 
-def num_bytes_to_struct_char(n: int):
+def num_bytes_to_struct_char(n: int) -> Optional[str]:
     """
     Given number of bytes, return the struct char that can hold those bytes.
+    Returns None on invalid value.
 
     For example,
         2 = H
         4 = I
     """
     if n > 8:
+        logger.warning("invalid format specifier: %d > 8", n)
         return None
-    if n > 4:
+    elif n > 4:
         return "Q"
-    if n > 2:
+    elif n > 2:
         return "I"
-    if n > 1:
+    elif n > 1:
         return "H"
-    if n == 1:
+    elif n == 1:
         return "B"
-    return None
+    else:
+        logger.warning("invalid format specifier: %d", n)
+        return None


Test patch:
diff --git a/tests/data b/tests/data
index 72de3ed..678338c 160000
--- a/tests/data
+++ b/tests/data
@@ -1,1 +1,1 @@
-Subproject commit 72de3eda1d8ec813e05d71d6d3d0d48e868d3de1
+Subproject commit 678338cdc68de965d68adb22099c380e48fc1f02
diff --git a/tests/test_invalid_streams.py b/tests/test_invalid_streams.py
new file mode 100644
index 0000000..521f8fb
--- /dev/null
+++ b/tests/test_invalid_streams.py
@@ -0,0 +1,28 @@
+import fixtures
+
+import dnfile
+
+
+def test_duplicate_stream():
+    path = fixtures.DATA / "invalid-streams" / "duplicate-stream.exe"
+
+    dn = dnfile.dnPE(path)
+
+    assert b"#US" in dn.net.metadata.streams
+    assert dn.net.user_strings.get_us(1).value == "BBBBBBBB"
+
+
+def test_unknown_stream():
+    path = fixtures.DATA / "invalid-streams" / "unknown-stream.exe"
+
+    dn = dnfile.dnPE(path)
+
+    assert b"#ZZ" in dn.net.metadata.streams
+
+
+def test_invalid_stream_name():
+    path = fixtures.DATA / "invalid-streams" / "invalid-stream-name.exe"
+
+    dn = dnfile.dnPE(path)
+
+    assert b"#\x90\x90" in dn.net.metadata.streams
diff --git a/tests/test_parse.py b/tests/test_parse.py
index 9806295..4a206af 100644
--- a/tests/test_parse.py
+++ b/tests/test_parse.py
@@ -1,14 +1,16 @@
+import pytest
 import fixtures
 
 import dnfile
+from dnfile.mdtable import TypeRefRow, AssemblyRefRow
 
 
 def test_metadata():
     path = fixtures.get_data_path_by_name("hello-world.exe")
 
     dn = dnfile.dnPE(path)
-
-    assert hasattr(dn, "net")
+    assert dn.net is not None
+    assert dn.net.metadata is not None
 
     dn.net.metadata.struct.Signature == 0x424A5342
     dn.net.metadata.struct.MajorVersion == 1
@@ -22,6 +24,8 @@ def test_streams():
     path = fixtures.get_data_path_by_name("hello-world.exe")
 
     dn = dnfile.dnPE(path)
+    assert dn.net is not None
+    assert dn.net.metadata is not None
 
     assert b"#~" in dn.net.metadata.streams
     assert hasattr(dn.net, "metadata")
@@ -32,7 +36,7 @@ def test_streams():
 
     # "user strings"
     assert b"#US" in dn.net.metadata.streams
-    # not sure where these are accessible yet
+    assert hasattr(dn.net, "user_strings")
 
     assert b"#GUID" in dn.net.metadata.streams
     assert hasattr(dn.net, "guids")
@@ -48,6 +52,7 @@ def test_tables():
     path = fixtures.get_data_path_by_name("hello-world.exe")
 
     dn = dnfile.dnPE(path)
+    assert dn.net is not None
 
     for table in ["Module", "TypeRef", "TypeDef", "MethodDef", "Param", "MemberRef", "CustomAttribute", "Assembly", "AssemblyRef"]:
         assert hasattr(dn.net.mdtables, table)
@@ -59,16 +64,18 @@ def test_module():
     path = fixtures.get_data_path_by_name("hello-world.exe")
 
     dn = dnfile.dnPE(path)
+    assert dn.net is not None
 
-    assert dn.net.mdtables.Module.rows[0].Name == "1-hello-world.exe"
+    assert dn.net.mdtables.Module[0].Name == "1-hello-world.exe"
 
 
 def test_typedef_extends():
     path = fixtures.get_data_path_by_name("hello-world.exe")
 
     dn = dnfile.dnPE(path)
+    assert dn.net is not None
 
-    typedefs = dn.net.mdtables.TypeDef.rows
+    typedefs = dn.net.mdtables.TypeDef
     assert typedefs[0].TypeName == "<Module>"
     assert typedefs[1].TypeName == "HelloWorld"
 
@@ -76,15 +83,19 @@ def test_typedef_extends():
     #      extends [mscorlib]System.Object
 
     extends = typedefs[1].Extends
+    assert extends.table is not None
     assert extends.table.name == "TypeRef"
     assert extends.row_index == 5
 
     superclass = extends.row
+    assert isinstance(superclass, TypeRefRow)
     assert superclass.TypeNamespace == "System"
     assert superclass.TypeName == "Object"
 
+    assert superclass.ResolutionScope.table is not None
     assert superclass.ResolutionScope.table.name == "AssemblyRef"
     assembly = superclass.ResolutionScope.row
+    assert isinstance(assembly, AssemblyRefRow)
     assert assembly.Name == "mscorlib"
 
 
@@ -92,8 +103,9 @@ def test_typedef_members():
     path = fixtures.get_data_path_by_name("hello-world.exe")
 
     dn = dnfile.dnPE(path)
+    assert dn.net is not None
 
-    typedefs = dn.net.mdtables.TypeDef.rows
+    typedefs = dn.net.mdtables.TypeDef
     assert typedefs[0].TypeName == "<Module>"
     assert typedefs[1].TypeName == "HelloWorld"
 
@@ -114,8 +126,9 @@ def test_method_params():
     path = fixtures.get_data_path_by_name("hello-world.exe")
 
     dn = dnfile.dnPE(path)
+    assert dn.net is not None
 
-    methods = dn.net.mdtables.MethodDef.rows
+    methods = dn.net.mdtables.MethodDef
     assert methods[0].Name == "Main"
     assert methods[1].Name == ".ctor"
 
@@ -124,4 +137,16 @@ def test_method_params():
     # instance default void '.ctor' ()  cil managed
     assert len(methods[1].ParamList) == 0
 
-    methods[0].ParamList[0].row.Name == "args"
+    assert methods[0].ParamList[0].row is not None
+    assert methods[0].ParamList[0].row.Name == "args"
+
+
+def test_ignore_NumberOfRvaAndSizes():
+    # .NET loaders ignores NumberOfRvaAndSizes, so attempt to parse anyways
+    path = fixtures.DATA / "1d41308bf4148b4c138f9307abc696a6e4c05a5a89ddeb8926317685abb1c241"
+    if not path.exists():
+        raise pytest.xfail("test file 1d41308bf41... (DANGER: malware) not found in test fixtures")
+
+    dn = dnfile.dnPE(path)
+    assert hasattr(dn, "net") and dn.net is not None
+    assert hasattr(dn.net, "metadata") and dn.net.metadata is not None
diff --git a/tests/test_utils.py b/tests/test_utils.py
index aac45f2..744736f 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,11 +1,11 @@
 # -*- coding: utf-8 -*-
-
-import dnfile
+import dnfile.utils
 
 
 def test_compressed_int():
     assert None is dnfile.utils.read_compressed_int(b"")
     assert None is dnfile.utils.read_compressed_int(None)
+
     assert 0x7f, 1 == dnfile.utils.read_compressed_int(b"\x7f")
     assert 0x3f8f, 2 == dnfile.utils.read_compressed_int(b"\xbf\x8f")
     assert 0x1eadbeef, 4 == dnfile.utils.read_compressed_int(b"\xde\xad\xbe\xef")