Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions lib/permittable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -744,14 +744,25 @@ def cast_string(value)
end
end

# Kernel#Integer/Float and BigDecimal() all accept underscore digit
# separators and surrounding whitespace — a convenience for a NUMBER
# LITERAL IN RUBY SOURCE, not for a request body. "1_8" is not how a
# client spells eighteen, and " 99 " is not how one spells ninety-nine;
# silently accepting either is the same kind of leniency as the
# NaN/Infinity/`0e10` cases below, just arriving from a different door.
# Checked against the raw String before any of those delegate, so a
# non-canonical spelling never reaches them at all.
INTEGER_FORMAT = /\A[+-]?\d+\z/
NUMERIC_FORMAT = /\A[+-]?(?:\d+(?:\.\d+)?|\.\d+)(?:[eE][+-]?\d+)?\z/

def cast_integer(value)
case value
when Integer then [:ok, value]
# NaN and Infinity first: `truncate` raises FloatDomainError on them (a
# RangeError, which the ArgumentError rescue below does not catch), and
# no integer is what either one sent. Same rule as finite_float.
when Float then value.finite? && value == value.truncate ? [:ok, value.to_i] : [:error, "invalid_type"]
when String then [:ok, Integer(value, 10)]
when String then value.match?(INTEGER_FORMAT) ? [:ok, Integer(value, 10)] : [:error, "invalid_type"]
else [:error, "invalid_type"]
end
rescue ArgumentError
Expand All @@ -761,7 +772,7 @@ def cast_integer(value)
def cast_float(value)
case value
when Numeric then finite_float(value.to_f)
when String then finite_float(Float(value), source: value)
when String then value.match?(NUMERIC_FORMAT) ? finite_float(Float(value), source: value) : [:error, "invalid_type"]
else [:error, "invalid_type"]
end
rescue ArgumentError
Expand Down Expand Up @@ -792,7 +803,8 @@ def nonzero_significand?(source)

def cast_decimal(value)
case value
when Numeric, String then finite_decimal(BigDecimal(value.to_s))
when Numeric then finite_decimal(BigDecimal(value.to_s))
when String then value.match?(NUMERIC_FORMAT) ? finite_decimal(BigDecimal(value)) : [:error, "invalid_type"]
else [:error, "invalid_type"]
end
rescue ArgumentError
Expand Down
18 changes: 18 additions & 0 deletions spec/permittable_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,15 @@ def include?(value) = any? { |candidate| candidate.to_s.include?(value.to_s) }
expect(violations_for({ n: "4.5" }) { permit_params(:create) { required :n, :integer } }.details.first[:code]).to eq("invalid_type")
end

it "rejects underscore digit separators and surrounding whitespace for :integer" do
# Integer() treats both as harmless formatting for a SOURCE LITERAL —
# neither is how a client spells a number in a request body.
["1_8", " 99 "].each do |value|
e = violations_for({ n: value }) { permit_params(:create) { required :n, :integer } }
expect(e.details).to eq([{ param: "n", code: "invalid_type" }]), "for #{value.inspect}"
end
end

it "accepts a whole Float for :integer but rejects a fractional one (JSON numbers)" do
klass = permittable_class { permit_params(:create) { required :n, :integer } }
expect(controller(klass, params: { n: 42.0 }).permitted_params[:n]).to eq(42)
Expand Down Expand Up @@ -696,6 +705,15 @@ def rejected(key, value, &decl)
end
end

it "rejects underscore digit separators and surrounding whitespace for :float and :decimal" do
# Float()/BigDecimal() treat both as harmless formatting for a SOURCE
# LITERAL, same leniency as the :integer version of this rule above.
["1_8.5", " 1.5 "].each do |value|
expect(rejected(:f, value, &decl)).to eq([{ param: "f", code: "invalid_type" }]), "for :float #{value.inspect}"
expect(rejected(:d, value, &decl)).to eq([{ param: "d", code: "invalid_type" }]), "for :decimal #{value.inspect}"
end
end

it "keeps accepting the large exponents BigDecimal genuinely represents" do
expect(permit({ d: "1e400" }, &decl)[:d]).to eq(BigDecimal("1e400"))
expect(permit({ d: "0.0000000000000000001" }, &decl)[:d]).to eq(BigDecimal("1e-19"))
Expand Down
Loading