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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased
Nil.
### Fixed
- Fixed slow matching in `ESCAPED_REGEX` on strings with many unclosed `<`, `{{`, `%{` or `&` (CWE-1333). Lazy wildcards became character classes bounded by each token's own delimiters. Output is unchanged or improved for well-formed input; a token containing a nested opener of its own kind (`<b <i>x</i>`, `{{ a {b} }}`, `&a&amp;`) is no longer swallowed up to the first closer. Quoted HTML attribute values may now contain `<` and `>`.

## [0.9.1] - 2024-06-27
### Added
Expand Down
8 changes: 4 additions & 4 deletions lib/pseudolocalization/pseudolocalizer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ module I18n
class Pseudolocalizer
ESCAPED_REGEX = Regexp.new("(#{
[
"<.*?>",
"{{.*?}}",
"%{.*?}",
"<[^<>\"'\\n]*(?:(?:\"[^\"\\n]*\"|'[^'\\n]*')[^<>\"'\\n]*)*>",
"{{[^{}\\n]*}}",
"%{[^{}\\n]*}",
"https?:\/\/\\S+",
"&\\S*?;"
"&[^\\s&;]*;"
].join('|')
})")

Expand Down
40 changes: 40 additions & 0 deletions test/pseudolocalization_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,46 @@ def test_it_works_with_templates
assert_equal('Ḥḛḛḽḽṓṓ, ẁṓṓṛḽḍ %{firstname} %{lastname}!', @backend.translate(:en, 'Hello, world %{firstname} %{lastname}!', {}))
end

def test_markup_never_spans_a_newline
assert_equal("αα <ṡṗααṇ\nͼḽααṡṡ=\"ẋ\">ḅ</span>", @backend.translate(:en, "a <span\nclass=\"x\">b</span>", {}))
assert_equal("αα {{\n ṇααṃḛḛ }} ḅ", @backend.translate(:en, "a {{\n name }} b", {}))
end

def test_unterminated_openers_are_pseudolocalized_as_text
assert_equal('αα <ḅ ͼ', @backend.translate(:en, 'a <b c', {}))
assert_equal('αα {{ ḅ ͼ', @backend.translate(:en, 'a {{ b c', {}))
assert_equal('αα %{ḅ ͼ', @backend.translate(:en, 'a %{b c', {}))
assert_equal('αα &ḅ ͼ', @backend.translate(:en, 'a &b c', {}))
end

def test_a_nested_opener_ends_the_outer_token
assert_equal('αα <ḅ <i>ͼ</i> ḍ', @backend.translate(:en, 'a <b <i>c</i> d', {}))
assert_equal('αα {{ ḅ | ϝ: {ͼ} }} ḍ', @backend.translate(:en, 'a {{ b | f: {c} }} d', {}))
assert_equal('αα %{ḅ{ͼ} ḍ', @backend.translate(:en, 'a %{b{c} d', {}))
assert_equal('αα &ḅ&amp; ͼ', @backend.translate(:en, 'a &b&amp; c', {}))
end

def test_quoted_attribute_values_can_contain_angle_brackets
assert_equal('<a title="a < b">ḽḭḭṇḳ</a>', @backend.translate(:en, '<a title="a < b">link</a>', {}))
assert_equal("<a title='a > b'>ḽḭḭṇḳ</a>", @backend.translate(:en, "<a title='a > b'>link</a>", {}))
end

def test_many_unterminated_openers_complete_in_linear_time
['<', '<"', "<'", '{{', '%{', '&'].each do |opener|
input = opener * 100_000
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
assert_equal(input, @backend.translate(:en, input, {}))
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
assert_operator elapsed, :<, 2.0, "#{opener.inspect} * 100_000 took #{elapsed.round(2)}s"
end
end

def test_escaped_regex_is_linear_time
skip 'Regexp.linear_time? was added in Ruby 3.2' unless Regexp.respond_to?(:linear_time?)

assert Regexp.linear_time?(Pseudolocalization::I18n::Pseudolocalizer::ESCAPED_REGEX)
end

def test_it_allows_ignoring_cetain_keys
@backend.ignores = ['Ignore*', /Clifford.$/]

Expand Down
Loading