pub struct ScopeSet(/* private fields */);Expand description
An ordered, deduplicated set of scope tokens. The wire form (both directions) is the RFC’s space-delimited string; ordering here is lexicographic so serialization is deterministic.
§Why a sorted Vec and not a BTreeSet
This was a BTreeSet<Scope> through 0.9.1, and the INVARIANT is unchanged: the tokens are held
sorted and deduplicated, so Display, Serialize, PartialEq and is_subset all answer
exactly what the tree answered. What changed is the code the invariant costs.
The set this type actually holds is tiny. A scope set is the scope parameter of one request or
the allowed_scopes of one client: single digits of tokens, each a handful of bytes. A B-tree
is machinery for a set that is large enough for the log factor to pay for the node bookkeeping,
and at these sizes it never does — it allocates a whole node to hold one token, and every
operation on it links a distinct instantiation of BTreeMap’s insert, clone and comparison
paths into the binary.
MEASURED 2026-08-13 by scripts/size-report.sh on the default row (aarch64-apple-darwin,
rustc 1.97.0): 15,394 bytes, taking the row from 234,623 to 219,229. That is not the tree’s
insert alone; it is every BTreeMap instantiation this type forced into a linked binary —
insert with its node splitting, clone_subtree, PartialEq::eq, is_subset’s range descent,
the iterators and the drop glue — none of which a host that only ever holds five scope tokens
was getting anything for. It is the same trade, for the same reason, as the one crate::store’s
barrier list records: an ordered container whose reads were a linear scan anyway does not need a
tree to be one.
It is also a much smaller heap footprint per stored record, which is the part that scales with
a deployment rather than with the binary. A BTreeSet allocates a whole leaf node the moment it
holds anything, and that node is the same size for one token as for eleven. MEASURED with
tests/support/alloc.rs, ScopeSet::parse:
| tokens | before | after |
|---|---|---|
| 1 | 2 allocs, 284 B | 2 allocs, 28 B |
| 3 | 4 allocs, 294 B | 4 allocs, 86 B |
Same allocation COUNT — the vector is sized once from the token count, so it does not trade
bytes for calls — and 90% fewer bytes at the size a real scope parameter actually is. Every
Client, IssuedToken, AuthorizationCodeRecord, RefreshTokenRecord, DeviceGrant and
consent record in a store was carrying one of those 256-byte nodes to hold a word or two.
Implementations§
Source§impl ScopeSet
impl ScopeSet
Sourcepub fn empty() -> Self
pub fn empty() -> Self
The empty set (serializes to the empty string; hosts normally omit the parameter instead).
Sourcepub fn parse(s: &str) -> Result<Self, InvalidScopeToken>
pub fn parse(s: &str) -> Result<Self, InvalidScopeToken>
Parse a space-delimited scope string. Repeated whitespace is tolerated; each token is charset-validated.
§There is NO cap on the token count, and that is a decision with a cost
MEASURED by benches/scaling.rs, both implementations on the same machine in the same
session, 2026-08-13:
| tokens | BTreeSet (through 0.9.1) | sorted Vec |
|---|---|---|
| 1 | 32.0 ns | 39.0 ns |
| 10 | 330 ns | 340 ns |
| 100 | 6.05 us | 4.41 us |
| 1000 | 81.09 us | 48.68 us |
The seven nanoseconds at one token are the pre-pass that COUNTS the tokens, and they are what
buys the single correctly-sized allocation; from a hundred tokens up the sort is the faster
structure by a wide margin. The growth is n log n either way, so this is not the accidental
quadratic that crate::server::MAX_RESOURCE_INDICATORS exists to bound; it is a
straightforward “how big may the parameter be” question, and reaching the top of that range
takes roughly ten kilobytes of scope, which a host’s own request-size limit is the right
place to refuse.
A cap here was considered and NOT taken, because it cannot be expressed without a breaking
change that is out of proportion to the problem: InvalidScopeToken is a tuple struct
with a public field, so it cannot gain a “too many” variant, and this same function is the
serde::Deserialize implementation for every persisted record that carries a scope, as
well as the constructor a host uses for its own allowed_scopes. A limit applied here would
therefore be a limit on what a deployment may REGISTER and on what it can read back out of
its own store, which is a different and much larger decision than bounding a request.
If a bound is wanted, the place for it is the wire boundary, alongside the other request caps, and it needs an error type this one cannot currently express.
Sourcepub fn from_tokens<I, T>(tokens: I) -> Result<Self, InvalidScopeToken>
pub fn from_tokens<I, T>(tokens: I) -> Result<Self, InvalidScopeToken>
Build from tokens, validating each.