public sealed class ResourceDirectoryParser
{
    public ResourceInfo? Parse(byte[] rawBytes, NtHeaders nt, IReadOnlyList<SectionInfo> sections, SafetyLimits limits)
    {
        if (nt.OptionalHeader.DataDirectories.Count <= 2)
        {
            return null;
        }

        var dir = nt.OptionalHeader.DataDirectories[2];
        if (dir.VirtualAddress == 0 || dir.Size == 0)
        {
            return null;
        }

        var baseOffset = RvaMapper.RvaToFileOffset(dir.VirtualAddress, sections, nt.OptionalHeader.SizeOfHeaders);
        if (baseOffset is null)
        {
            return null;
        }

        BoundsChecker.EnsureRange(baseOffset.Value, 16, rawBytes.Length, "resource directory root");

        var root = ParseDirectory(rawBytes, nt, sections, limits, dir.VirtualAddress, baseOffset.Value, 0, null, null);
        return new ResourceInfo(root);
    }

    private ResourceNode ParseDirectory(
        byte[] rawBytes,
        NtHeaders nt,
        IReadOnlyList<SectionInfo> sections,
        SafetyLimits limits,
        uint resourceBaseRva,
        int resourceBaseOffset,
        int relativeOffset,
        string? name,
        ushort? id,
        int depth = 0)
    {
        if (depth > limits.MaxResourceTreeRecursionDepth)
            throw new SafetyException("Resource tree recursion depth exceeded.");

        var offset = checked(resourceBaseOffset + relativeOffset);
        BoundsChecker.EnsureRange(offset, 16, rawBytes.Length, "resource directory header");

        var numberOfNamedEntries = BitConverter.ToUInt16(rawBytes, offset + 12);
        var numberOfIdEntries = BitConverter.ToUInt16(rawBytes, offset + 14);
        var total = numberOfNamedEntries + numberOfIdEntries;
        BoundsChecker.EnsureCount(total, limits.MaxImportsCount, "Resource entries");

        var children = new List<ResourceNode>();
        var entryOffset = checked(offset + 16);

        for (var i = 0; i < total; i++)
        {
            var currentEntryOffset = checked(entryOffset + i * 8);
            BoundsChecker.EnsureRange(currentEntryOffset, 8, rawBytes.Length, "resource directory entry");

            var nameOrId = BitConverter.ToUInt32(rawBytes, currentEntryOffset);
            var dataOrSubdir = BitConverter.ToUInt32(rawBytes, currentEntryOffset + 4);

            string? childName = null;
            ushort? childId = null;

            if ((nameOrId & 0x80000000) != 0)
            {
                var strRel = (int)(nameOrId & 0x7FFFFFFF);
                var strOffset = checked(resourceBaseOffset + strRel);
                BoundsChecker.EnsureRange(strOffset, 2, rawBytes.Length, "resource unicode length");

                var len = BitConverter.ToUInt16(rawBytes, strOffset);
                BoundsChecker.EnsureRange(strOffset + 2, checked((long)len * 2), rawBytes.Length, "resource unicode string");
                childName = BinaryReaderExtensions.ReadUnicodeStringAt(rawBytes, strOffset + 2, len);
            }
            else
            {
                childId = (ushort)(nameOrId & 0xFFFF);
            }

            if ((dataOrSubdir & 0x80000000) != 0)
            {
                var subdirRel = (int)(dataOrSubdir & 0x7FFFFFFF);
                children.Add(ParseDirectory(rawBytes, nt, sections, limits, resourceBaseRva, resourceBaseOffset, subdirRel, childName, childId, depth + 1));
            }
            else
            {
                var dataEntryOffset = checked(resourceBaseOffset + (int)dataOrSubdir);
                BoundsChecker.EnsureRange(dataEntryOffset, 16, rawBytes.Length, "resource data entry");

                var dataRva = BitConverter.ToUInt32(rawBytes, dataEntryOffset);
                var size = BitConverter.ToUInt32(rawBytes, dataEntryOffset + 4);
                var codePage = BitConverter.ToUInt32(rawBytes, dataEntryOffset + 8);
                var reserved = BitConverter.ToUInt32(rawBytes, dataEntryOffset + 12);

                children.Add(new ResourceNode(
                    childName,
                    childId,
                    false,
                    Array.Empty<ResourceNode>(),
                    new ResourceDataInfo(dataRva, size, codePage, reserved)));
            }
        }

        return new ResourceNode(name, id, true, children, null);
    }
}