Skip to content

Commit 88cdef4

Browse files
committed
Report over-cap parser input as a structured :input_too_large error
1 parent 347ef93 commit 88cdef4

10 files changed

Lines changed: 95 additions & 383 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
1010

1111
* `Localize.LanguageTag.best_match/3` now prefers a territory's own language when candidates are otherwise equidistant, so `sgs` (Samogitian, spoken in Lithuania) matches `lt` rather than `en-LT`. The distance trie scores every candidate sharing the desired script and territory identically, which previously left the winner to depend on the order of the supported list.
1212

13+
* `Localize.ParseError` reports an over-cap input as `reason: :input_too_large` with the byte counts in new `:size` and `:limit` fields, instead of interpolating them into a prose `:reason` string that its own type never allowed. The message, number and unit-identifier parsers all use it, and the oversized input is no longer retained on the exception.
14+
1315
## [1.0.0] — July 31st, 2026
1416

1517
The first stable release. MF2 message validation is separated from parsing: `Localize.Message.Parser.parse/1` checks syntax, `Localize.Message.Validator.validate/1` checks the TR35 data-model rules, and everything that formats or serializes a message runs both. A message like `.local $count = {$count :number}` — a declaration that reads the variable it declares, which is a Duplicate Declaration error rather than a valid annotation of an external variable — is now rejected wherever it appears rather than only when formatted. The construct for annotating an external variable is `.input {$count :number}`.

lib/localize/exception/parse_error.ex

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,24 @@ defmodule Localize.ParseError do
2020
`:cause` carries an underlying exception when a higher-level parser
2121
has wrapped a lower-level one.
2222
23+
When `:reason` is `:input_too_large` the parse was refused before it
24+
began because the input exceeded a configured byte cap. `:size` and
25+
`:limit` carry the byte counts and `:detail` names what was being
26+
parsed. `:input` is `nil` in this case — retaining the oversized
27+
string is the thing the cap exists to prevent.
28+
2329
"""
2430

2531
@behaviour Localize.Exception
2632

27-
defexception [:input, :reason, :offset, :line, :column, :rest, :detail, :cause]
33+
defexception [:input, :reason, :offset, :line, :column, :rest, :detail, :cause, :size, :limit]
2834

2935
@type reason ::
3036
:unexpected_trailing_input
3137
| :unexpected_input
3238
| :incomplete_input
3339
| :invalid_message_format
40+
| :input_too_large
3441

3542
@type t :: %__MODULE__{
3643
input: String.t() | nil,
@@ -40,7 +47,9 @@ defmodule Localize.ParseError do
4047
column: pos_integer() | nil,
4148
rest: String.t() | nil,
4249
detail: String.t() | nil,
43-
cause: Exception.t() | nil
50+
cause: Exception.t() | nil,
51+
size: non_neg_integer() | nil,
52+
limit: non_neg_integer() | nil
4453
}
4554

4655
@impl Localize.Exception
@@ -49,7 +58,8 @@ defmodule Localize.ParseError do
4958
:unexpected_trailing_input,
5059
:unexpected_input,
5160
:incomplete_input,
52-
:invalid_message_format
61+
:invalid_message_format,
62+
:input_too_large
5363
]
5464

5565
@impl true
@@ -150,6 +160,20 @@ defmodule Localize.ParseError do
150160
)
151161
end
152162

163+
# The oversized input is deliberately not retained on the struct — it
164+
# is the thing we refused to hold. `:size` and `:limit` carry the byte
165+
# counts and `:detail` names what was being parsed.
166+
def message(%__MODULE__{reason: :input_too_large, size: size, limit: limit, detail: detail})
167+
when is_integer(size) and is_integer(limit) do
168+
Localize.Exception.safe_message(
169+
"message",
170+
"The {$detail} is {$size} bytes, which exceeds the configured maximum of {$limit} bytes",
171+
detail: detail_or_default(detail, "input"),
172+
size: size,
173+
limit: limit
174+
)
175+
end
176+
153177
def message(%__MODULE__{reason: :incomplete_input, input: input}) do
154178
Localize.Exception.safe_message(
155179
"message",
@@ -187,6 +211,9 @@ defmodule Localize.ParseError do
187211
defp detail_or_default(nil), do: ""
188212
defp detail_or_default(detail) when is_binary(detail), do: detail
189213

214+
defp detail_or_default(nil, default), do: default
215+
defp detail_or_default(detail, _default) when is_binary(detail), do: detail
216+
190217
defp rest_suffix(nil), do: ""
191218
defp rest_suffix(""), do: ""
192219
defp rest_suffix(rest) when is_binary(rest), do: " (remaining: #{inspect(rest)})"

lib/localize/message/parser/parser.ex

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ defmodule Localize.Message.Parser do
3434
if byte_size(input) > cap do
3535
{:error,
3636
Localize.ParseError.exception(
37-
input: "<#{byte_size(input)}-byte message>",
38-
reason: "message exceeds the configured maximum of #{cap} bytes"
37+
reason: :input_too_large,
38+
detail: "message",
39+
size: byte_size(input),
40+
limit: cap
3941
)}
4042
else
4143
do_parse(input)

lib/localize/number/parser.ex

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,10 @@ defmodule Localize.Number.Parser do
110110
if byte_size(string) > cap do
111111
{:error,
112112
Localize.ParseError.exception(
113-
input: "<#{byte_size(string)}-byte number>",
114-
reason: "number string exceeds the configured maximum of #{cap} bytes"
113+
reason: :input_too_large,
114+
detail: "number string",
115+
size: byte_size(string),
116+
limit: cap
115117
)}
116118
else
117119
do_parse(string, options)

lib/localize/unit/parser.ex

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,10 @@ defmodule Localize.Unit.Parser do
4141
byte_size(input) > cap ->
4242
{:error,
4343
Localize.ParseError.exception(
44-
input: "<#{byte_size(input)}-byte unit identifier>",
45-
reason: "unit identifier exceeds the configured maximum of #{cap} bytes"
44+
reason: :input_too_large,
45+
detail: "unit identifier",
46+
size: byte_size(input),
47+
limit: cap
4648
)}
4749

4850
custom_unit_name?(input) ->

0 commit comments

Comments
 (0)