Layout.init(0) is an explicitly supported special case that produces a
valid zero-capacity set with a zero-size table. But lookupContext had
no guard for it: probing computes `table[hash & 0]` and reads whatever
memory follows the set in its backing buffer, treating those bytes as
an item ID which is then used to index the (also zero-size) items
array, an out-of-bounds read reaching arbitrarily far past the set.
This has never fired in practice because no production page carries a
zero-capacity set today, and where one could occur the adjacent bytes
happen to be zero (which reads as an empty bucket and returns null).
Page.exactRowCapacity legitimately produces zero capacities for pages
without styled or hyperlinked cells though, so any page compaction
work makes this reachable with nonzero adjacent memory: in a Page
layout the styles set can be followed by the grapheme bitmap, which
is initialized to all ones.
Lookups on a zero-capacity set now return null without touching the
table. This also covers add, which looks up before inserting and
already handles the zero capacity correctly after that point by
returning OutOfMemory. All other entry points assert on valid IDs,
which a zero-capacity set cannot have.
As of Zig 0.14.0, `@splat` can be used for array types, which eliminates
a lot of redundant syntax and makes things generally cleaner.
I've explicitly avoided applying this change in the renderer files for
now since it would just create rebasing conflicts in my renderer rework
branch which I'll be PR-ing pretty soon.
Poor hash uniformity and/or a crafted or unlucky input could cause the
bounds of the PSL stats array to be exceeded, which caused memory
corruption (not good!) -- we avoid such cases now by returning an
OutOfMemory error if we're about to insert and there's an item with a
PSL in the last slot.
Previous logic had multiple issues that were hiding in edge cases of
edge cases with the ressurected item handling among other things; the
added assertIntegrity method finds these issues, the primary one being
an edge case where an ID is present in two different buckets.
Added more comments to explain logic in more detail and fixed a couple
little things like always using `+%` when incrementing the probe pos,
and replacing a silent return on an integrity issue that should be
impossible (`table[item.meta.bucket] != id`) with an assert.
This enables these funcs to access memory offsets that may be present in
set items, which is possible since the set itself is in an offset-based
structure.
Prevent bad input from causing repeated OutOfMemory errors by erroring
with NeedsRehash instead when there are unused dead IDs available.
Additionally, properly decrement PSL stats when reviving dead IDs.