Check length: before in: and format: - #25
Open
VSN2015 wants to merge 1 commit into
Open
Conversation
VSN2015
commented
Sep 5, 2026
| # failure first is the better answer anyway — a client cannot act on | ||
| # "wrong format" for a value that is also far too long. | ||
| def check_scalar_rules(field, value) | ||
| return [:error, "length"] if field[:length] && !length_ok?(field[:length], value.length) |
Owner
Author
There was a problem hiding this comment.
Checking length: before in: and format: protects regex matching from having to evaluate giant string inputs when the length bound has already failed.
| end | ||
| end.new("\\A[a-z]+\\z") | ||
| end | ||
|
|
Owner
Author
There was a problem hiding this comment.
Using a spy_format subclass of Regexp to verify that match? is never invoked on length-violating inputs is a great test design.
length: is an O(1) read of a String's size. format: runs a regexp over
the whole value and validate: runs arbitrary app code. Checking the
cheap bound LAST meant a value the bound already excluded still paid
for the expensive ones:
5 MB string against `length: 1..80, format: EMAIL_REGEXP`
before: 121 ms — the regexp scanned all 5 MB (82 ms of it) and the
result was then discarded by the length check
after: 38 ms — the regexp is never consulted
With a well-behaved regexp that is waste. With an app regexp that has
poor worst-case behaviour it is a lever, handed to whoever can send a
long string to a bounded field — which is every field that declares a
length at all.
The documented order is now
normalize: -> cast -> length: -> in: -> format: -> validate:
with the first failure reported. The only observable change is which
code a value violating BOTH reports: length rather than
inclusion/format. That is the better answer anyway — a client cannot
act on "wrong format" for a value that is also far too long.
The spec proves the regexp is genuinely skipped rather than merely
outranked, using a Regexp subclass that records whether it was
consulted, so it satisfies any format: type check while still being
observable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
VSN2015
force-pushed
the
fix/check-length-before-format
branch
from
September 11, 2026 22:01
2b182e6 to
d6b2207
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
length:is an O(1) read of a String's size.format:runs a regexp over the whole value;validate:runs arbitrary app code. Checking the cheap bound last meant a value the bound already excluded still paid for the expensive ones.A 5 MB string against an entirely ordinary declaration:
HashWithIndifferentAccessconversion of the payload)With a well-behaved regexp that is waste. With an app regexp that has poor worst-case behaviour it is a lever, available to anyone who can send a long string to a bounded field — which is every field that declares a length at all.
The fix
First failure reported, and the order is now documented in the README.
The one observable change
A value violating both its length and its format reports
lengthinstead offormat/inclusion. That's the more useful answer anyway — a client can't act on "wrong format" for a value that is also far too long.Nothing else moves: a field with no
length:checksformat:exactly as before, and a value inside its length bound still gets the full set of checks in order.How the spec proves it
Asserting the reported code would only show
formatwas outranked, not that it was skipped. So the spec passes aRegexpsubclass that records whethermatch?was called — satisfying anyformat:type check while staying observable:and the companion spec asserts it is consulted for a value within the bound.
Verification