Skip to content

Key-transform wrappers delegate capability methods with the unmapped key (url_for etc. silently address the wrong object) #83

Description

@thorwhalen

Summary

When a store is wrapped with a key transform (KeyCodecs.prefixed, wrap_kvs(id_of_key=...), Pipe of those), the wrapper's __getitem__/__setitem__ correctly map the key — but any other method delegates to the inner store with the outer, unmapped key. dol.base.Store.__getattr__ returns the bound leaf method, so the leaf receives a key it was never meant to see.

For a backend adapter this is severe, because the interesting capabilities are exactly the non-dunder methods.

Repro

from dol import KeyCodecs

class WithUrl(dict):
    def url_for(self, k): return f"https://x/{k}"

w = KeyCodecs.prefixed('a/')(WithUrl)({'a/b': 1})

w['b']            # -> 1                  correct, prefix applied
w.url_for('b')    # -> 'https://x/b'      WRONG: should be 'https://x/a/b'

Measured on a realistic adapter (a store scoped to logs/, in a bucket that also holds logs2/leak and a root a.txt):

method through the wrap consequence
url_for('a.txt') https://…/a.txt signs a URL for the wrong object
sub('x/') store over the root x/ scope escape
handle('a.txt') handle on root a.txt wrong object
info('a.txt') KeyError for a key that is present manufactured "absent"
prefixes() leaks the sibling prefix the scope exists to hide
delete_many(['a.txt']) root a.txt destroyed silent destruction of the wrong object

Nothing raises, and isinstance(w, SupportsUrlFor) stays True — a @runtime_checkable Protocol checks method presence only, so it cannot detect the breakage. (It is also wrap-dependent: since 3.12 isinstance uses getattr_static, which sees a class-wrapped capability but not an instance-wrapped one.)

The existing answer, and why it's easy to get wrong

dol.wrapped_self (shipped in 0.3.58) is the fix, but the natural formulation fails silently:

# inside a delegated method, `self` IS the unwrapped leaf (issue #18)
inner_most_key(self, k)                  # -> None      <- silent
inner_most_key(wrapped_self(self), k)    # -> 'a/b'     correct

inner_most_key(self, k) returns None because store_trans_path yields nothing when the store lacks _id_of_key, and last_element returns None. So the URL becomes https://…/None with no exception.

Ask

Two things, either of which would help a lot:

  1. Document wrapped_self as the answer for capability methods on wrapped stores, with the inner_most_key(wrapped_self(self), k) form spelled out — it is currently discoverable only if you already know issue wrap_kvs will wrap the instance but self of instance is not wrapped #18 exists.
  2. Consider a declarative key-mapped delegation helper, so an adapter can say "these methods take a key as their first argument, map it on the way in" rather than each method remembering:
@delegate_with_mapped_key('url_for', 'info', 'handle', 'sub')
class BucketReader(...): ...

Every *dol adapter that adds a keyed capability method has this latent bug; a declarative form fixes the family at once. Related: inner_most_key(self, k) returning None rather than raising is itself worth changing — a silent None key is a bug amplifier.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions