Skip to content
Open
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
27 changes: 23 additions & 4 deletions fluent.runtime/fluent/runtime/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,17 @@ def __call__(self, env: ResolverEnvironment) -> Union[str, FluentNone]:
raise ValueError(
"Too many parts in message (> {}), " "aborting.".format(self.MAX_PARTS)
)
retval = "".join(resolve(element(env), env) for element in elements)
# Text written directly in the pattern is not subject to the
# MAX_PART_LENGTH limit, which protects against placeable expansion.
# Placeables have already resolved (and checked) their own value.
retval = "".join(
(
element(env)
if isinstance(element, (Literal, Placeable))
else resolve(element(env), env)
)
for element in elements
)
env.part_count += len(elements)
env.active_patterns.remove(self)
return retval
Expand All @@ -174,11 +184,20 @@ def resolve(fluentish: Any, env: ResolverEnvironment) -> Any:
if len(fluentish) > MAX_PART_LENGTH:
raise ValueError(
"Too many characters in placeable "
"({}, max allowed is {})".format(len(fluentish), Pattern.MAX_PARTS)
"({}, max allowed is {})".format(len(fluentish), MAX_PART_LENGTH)
)
return fluentish


def resolve_expression(expression: BaseResolver, env: ResolverEnvironment) -> Any:
value = expression(env)
if isinstance(expression, SelectExpression) and isinstance(value, str):
# The selected variant is a pattern of the same message, which has
# already been resolved with its placeables subject to MAX_PART_LENGTH.
return value
return resolve(value, env)


class TextElement(FTL.TextElement, Literal):
value: str

Expand All @@ -190,7 +209,7 @@ class Placeable(FTL.Placeable, BaseResolver):
expression: Union["InlineExpression", "Placeable", "SelectExpression"]

def __call__(self, env: ResolverEnvironment) -> Any:
inner = resolve(self.expression(env), env)
inner = resolve_expression(self.expression, env)
if not env.context.use_isolating:
return inner
return "\u2068" + inner + "\u2069"
Expand All @@ -200,7 +219,7 @@ class NeverIsolatingPlaceable(FTL.Placeable, BaseResolver):
expression: Union["InlineExpression", Placeable, "SelectExpression"]

def __call__(self, env: ResolverEnvironment) -> Any:
inner = resolve(self.expression(env), env)
inner = resolve_expression(self.expression, env)
return inner


Expand Down
53 changes: 53 additions & 0 deletions fluent.runtime/tests/test_bomb.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def test_max_length_protection(self, bundle):
assert val == "{???}"
assert len(errs) != 0
assert "Too many characters" in str(errs[-1])
assert "max allowed is 2500" in str(errs[-1])

def test_max_expansions_protection(self, bundle):
# Without protection, emptylolz will take a really long time to
Expand All @@ -42,3 +43,55 @@ def test_max_expansions_protection(self, bundle):
assert val == "{???}"
assert len(errs) == 1
assert "Too many parts" in str(errs[-1])


class TestLongText:
# The length limit protects against expansion of placeables, and
# should not apply to text written directly in the message.
@pytest.mark.parametrize("use_isolating", [True, False])
def test_long_text_with_placeable(self, use_isolating):
long_text = "x" * 3000
bundle = FluentBundle(["en-US"], use_isolating=use_isolating)
bundle.add_resource(FluentResource(f"long = {long_text} {{ $arg }}\n"))
val, errs = bundle.format_pattern(
bundle.get_message("long").value, {"arg": "y"}
)
y = "\u2068y\u2069" if use_isolating else "y"
assert val == f"{long_text} {y}"
assert errs == []

def test_long_string_literal(self):
long_text = "x" * 3000
bundle = FluentBundle(["en-US"], use_isolating=False)
bundle.add_resource(FluentResource(f'long = {{ "{long_text}" }} {{ $arg }}\n'))
val, errs = bundle.format_pattern(
bundle.get_message("long").value, {"arg": "y"}
)
assert val == f"{long_text} y"
assert errs == []

@pytest.mark.parametrize("n, expected", [(1, "{text}"), (2, "{text} 2")])
def test_long_text_in_select_variant(self, n, expected):
long_text = "x" * 3000
bundle = FluentBundle(["en-US"], use_isolating=False)
bundle.add_resource(
FluentResource(
f"long = {{ $n ->\n"
f" [one] {long_text}\n"
f" *[other] {long_text} {{ $n }}\n"
f"}}\n"
)
)
val, errs = bundle.format_pattern(bundle.get_message("long").value, {"n": n})
assert val == expected.format(text=long_text)
assert errs == []

def test_long_select_variant_after_text(self):
long_text = "x" * 3000
bundle = FluentBundle(["en-US"], use_isolating=False)
bundle.add_resource(
FluentResource(f"long = Hi {{ $n ->\n *[other] {long_text}\n}}\n")
)
val, errs = bundle.format_pattern(bundle.get_message("long").value, {"n": 1})
assert val == f"Hi {long_text}"
assert errs == []