crass 1.0.6 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: def285e9f3d16e7222dce13f004b8ad8b936c76ecec909306cf15078f6ec8999
4
- data.tar.gz: 6fbb08644b3ff290d66012bbfa9afa249ab565f9e67240e13d433715d1b535b7
3
+ metadata.gz: 4519ecea822a7a2632913d4ed71cb69c7258f52be999546e0cf6c3e07b3c1977
4
+ data.tar.gz: 679720a3e80e1cd1b7a9a2e74918006deb3f515f1f1adef72675f2e34b031231
5
5
  SHA512:
6
- metadata.gz: 43daba64a022574e3ac4b439c5dca3cc329e2217e47df9eeaf79987748fa949feeaccb4745a212ebb9e91f01d4b5ec433947b57380efda233d078a9f1f1ea8ab
7
- data.tar.gz: aa88544b1374fa5fb774970e5b714e6913c23e750a0e84767548e9c8076d25851f9f8861b38dfef0c9262a8c7785bce82436ad19c951ea4a9836fcee4a163199
6
+ metadata.gz: f10e1e6862c2af99e8a703695cd88576cbe67d4bfd12f11d2eaed16d5a2d0bc1662a515b1e43e2e7371a0bef42f86a7af04dc1f5c4a84fe2136d7c6bebae0f47
7
+ data.tar.gz: c55f035f81dbaa5a3a0abfd0fcbb2636ec151ae24e50806e3bcd871a10af281a35b3d892281a15bade02fb36bc38de945fa547110c74442e2fac3f86b910a204
@@ -0,0 +1,25 @@
1
+ name: Tests
2
+ on: [ push, pull_request ]
3
+ jobs:
4
+ test:
5
+ strategy:
6
+ fail-fast: false
7
+ matrix:
8
+ os:
9
+ - ubuntu-latest
10
+ - macos-latest
11
+ ruby:
12
+ - '3.3'
13
+ - '3.4'
14
+ - jruby
15
+ - ruby
16
+ - ruby-head
17
+ continue-on-error: ${{ endsWith(matrix.ruby, 'head') }}
18
+ runs-on: ${{ matrix.os }}
19
+ steps:
20
+ - uses: actions/checkout@v7
21
+ - uses: ruby/setup-ruby@9eb537ca036ebaed86729dcb9309076e4c5c3b74 # v1.314.0
22
+ with:
23
+ bundler-cache: true
24
+ ruby-version: ${{ matrix.ruby }}
25
+ - run: bundle exec rake
data/HISTORY.md CHANGED
@@ -1,128 +1,95 @@
1
- Crass Change History
2
- ====================
1
+ # Crass Changelog
3
2
 
4
- 1.0.6 (2020-01-12)
5
- ------------------
3
+ ## 1.0.8 (2026-10-21)
6
4
 
7
- * Number values are now limited to a maximum of `Float::MAX` and a minimum of
8
- negative `Float::MAX`. (#11)
5
+ ### Security
9
6
 
10
- * Added project metadata to the gemspec. (#9 - @orien)
7
+ - Moderate: Fixed a tokenizer bug in which a `)` following an escaped backslash (`\\`) inside a bad URL didn't terminate the bad URL token, causing Crass to consume more input than a spec-compliant browser would. CSS sanitizers built on Crass could be tricked into passing through declarations, rules, or at-rules that a browser would then apply. (GHSA-pq4g-8rxf-4g29)
11
8
 
9
+ ## 1.0.7 (2026-06-25)
12
10
 
13
- 1.0.5 (2019-10-15)
14
- ------------------
11
+ ### Security
15
12
 
16
- * Removed test files from the gem. (#8 - @t-richards)
13
+ - High: Fixed a denial of service vulnerability in which a large numeric exponent could consume disproportionate CPU and memory before the value was clamped. Exponents are now bounded before `10**exponent` is computed. (GHSA-6wmf-3r64-vcwv)
17
14
 
15
+ - Moderate: Fixed a scenario in which deeply nested simple blocks or functions could exhaust the Ruby stack and raise `SystemStackError`, or could result in excessive memory usage. Parser nesting is now limited to a configurable maximum depth via a new option (`:maximum_depth`, with a conservative default of 25). Constructs nested more deeply are discarded as an `:error` node with the value "maximum-depth-exceeded". (GHSA-6jxj-px6v-747w)
18
16
 
19
- 1.0.4 (2018-04-08)
20
- ------------------
17
+ - Moderate: Fixed a scenario in which a long run of adjacent comments could exhaust the Ruby stack and raise `SystemStackError`. Discarded comments are now skipped iteratively rather than recursively. (GHSA-wwpr-jff3-395c)
21
18
 
22
- * Fixed whitespace warnings. (#7 - @yahonda)
19
+ - Moderate: Fixed a denial of service vulnerability in which inputs containing many non-ASCII characters could cause excessive CPU usage due to inefficient handling of multi-byte characters during tokenization. (GHSA-8vfg-2r28-hvhj)
23
20
 
21
+ ## 1.0.6 (2020-01-12)
24
22
 
25
- 1.0.3 (2017-11-13)
26
- ------------------
23
+ - Number values are now limited to a maximum of `Float::MAX` and a minimum of negative `Float::MAX`. (#11)
27
24
 
28
- * Added support for frozen string literals. (#3 - @flavorjones)
25
+ - Added project metadata to the gemspec. (#9 - @orien)
29
26
 
27
+ ## 1.0.5 (2019-10-15)
30
28
 
31
- 1.0.2 (2015-04-17)
32
- ------------------
29
+ - Removed test files from the gem. (#8 - @t-richards)
33
30
 
34
- * Fixed: An at-rule immediately followed by a `{}` simple block would have the
35
- block (and subsequent tokens until a semicolon) incorrectly appended to its
36
- prelude. This was super dumb and made me very sad.
31
+ ## 1.0.4 (2018-04-08)
37
32
 
33
+ - Fixed whitespace warnings. (#7 - @yahonda)
38
34
 
39
- 1.0.1 (2014-11-16)
40
- ------------------
35
+ ## 1.0.3 (2017-11-13)
41
36
 
42
- * Fixed: Modifications made to the block of an `:at_rule` node in a parse tree
43
- weren't reflected when that node was stringified. This was a regression
44
- introduced in 1.0.0.
37
+ - Added support for frozen string literals. (#3 - @flavorjones)
45
38
 
39
+ ## 1.0.2 (2015-04-17)
46
40
 
47
- 1.0.0 (2014-11-16)
48
- ------------------
41
+ - Fixed: An at-rule immediately followed by a `{}` simple block would have the block (and subsequent tokens until a semicolon) incorrectly appended to its prelude. This was super dumb and made me very sad.
49
42
 
50
- * Many parsing and tokenization tweaks to bring us into full compliance with the
51
- [14 November 2014 editor's draft][css-syntax-draft] of the CSS syntax spec.
52
- The most significant outwardly visible change is that quoted URLs like
53
- `url("foo")` are now returned as `:function` tokens and not `:url` tokens due
54
- to a change in the tokenization spec.
43
+ ## 1.0.1 (2014-11-16)
55
44
 
56
- * Teensy tiny speed and memory usage improvements that you almost certainly
57
- won't notice.
45
+ - Fixed: Modifications made to the block of an `:at_rule` node in a parse tree weren't reflected when that node was stringified. This was a regression introduced in 1.0.0.
58
46
 
59
- * Fixed: A semicolon following a `@charset` rule would be omitted during
60
- serialization.
47
+ ## 1.0.0 (2014-11-16)
61
48
 
62
- * Fixed: A multibyte char at the beginning of an id token could trigger an
63
- encoding error because `StringScanner#peek` is a jerkface.
49
+ - Many parsing and tokenization tweaks to bring us into full compliance with the [14 November 2014 editor's draft](http://dev.w3.org/csswg/css-syntax-3/) of the CSS syntax spec. The most significant outwardly visible change is that quoted URLs like `url("foo")` are now returned as `:function` tokens and not `:url` tokens due to a change in the tokenization spec.
64
50
 
65
- [css-syntax-draft]:http://dev.w3.org/csswg/css-syntax-3/
51
+ - Teensy tiny speed and memory usage improvements that you almost certainly won't notice.
66
52
 
53
+ - Fixed: A semicolon following a `@charset` rule would be omitted during serialization.
67
54
 
68
- 0.2.1 (2014-07-22)
69
- ------------------
55
+ - Fixed: A multibyte char at the beginning of an id token could trigger an encoding error because `StringScanner#peek` is a jerkface.
70
56
 
71
- * Fixed: Error when the last property of a rule has no value and no terminating
72
- semicolon. [#2][]
57
+ ## 0.2.1 (2014-07-22)
73
58
 
74
- [#2]:https://github.com/rgrove/crass/issues/2
59
+ - Fixed: Error when the last property of a rule has no value and no terminating semicolon. [#2](https://github.com/rgrove/crass/issues/2)
75
60
 
61
+ ## 0.2.0 (2013-10-10)
76
62
 
77
- 0.2.0 (2013-10-10)
78
- ------------------
63
+ - Added a `:children` field to `:property` nodes. It's an array containing all the nodes that make up the property's value.
79
64
 
80
- * Added a `:children` field to `:property` nodes. It's an array containing all
81
- the nodes that make up the property's value.
65
+ - Fixed: Incorrect value was given for `:property` nodes whose values contained functions.
82
66
 
83
- * Fixed: Incorrect value was given for `:property` nodes whose values contained
84
- functions.
67
+ - Fixed: When parsing the value of an at-rule's block as a list of rules, a selector containing a function (such as `#foo:not(.bar)`) would cause that property and the rest of the token stream to be discarded.
85
68
 
86
- * Fixed: When parsing the value of an at-rule's block as a list of rules, a
87
- selector containing a function (such as `#foo:not(.bar)`) would cause that
88
- property and the rest of the token stream to be discarded.
69
+ ## 0.1.0 (2013-10-04)
89
70
 
71
+ - Tokenization is a little over 50% faster.
90
72
 
91
- 0.1.0 (2013-10-04)
92
- ------------------
73
+ - Added tons of unit tests.
93
74
 
94
- * Tokenization is a little over 50% faster.
75
+ - Added `Crass.parse_properties` and `Crass::Parser.parse_properties`, which can be used to parse the contents of an HTML element's `style` attribute.
95
76
 
96
- * Added tons of unit tests.
77
+ - Added `Crass::Parser.parse_rules`, which can be used to parse the contents of an `:at_rule` block like `@media` that may contain style rules.
97
78
 
98
- * Added `Crass.parse_properties` and `Crass::Parser.parse_properties`, which can
99
- be used to parse the contents of an HTML element's `style` attribute.
79
+ - Fixed: `Crass::Parser#consume_at_rule` and `#consume_qualified_rule` didn't properly handle already-parsed `:simple_block` nodes in the input, which occurs when parsing rules in the value of an `:at_rule` block.
100
80
 
101
- * Added `Crass::Parser.parse_rules`, which can be used to parse the contents of
102
- an `:at_rule` block like `@media` that may contain style rules.
81
+ - Fixed: On `:property` nodes, `:important` is now set to `true` when the property is followed by an "!important" declaration.
103
82
 
104
- * Fixed: `Crass::Parser#consume_at_rule` and `#consume_qualified_rule` didn't
105
- properly handle already-parsed `:simple_block` nodes in the input, which
106
- occurs when parsing rules in the value of an `:at_rule` block.
83
+ - Fixed: "!important" is no longer included in the value of a `:property` node.
107
84
 
108
- * Fixed: On `:property` nodes, `:important` is now set to `true` when the
109
- property is followed by an "!important" declaration.
85
+ - Fixed: A variety of tokenization bugs uncovered by tests.
110
86
 
111
- * Fixed: "!important" is no longer included in the value of a `:property` node.
87
+ - Fixed: Added a workaround for a possible spec bug when an `:at_keyword` is encountered while consuming declarations.
112
88
 
113
- * Fixed: A variety of tokenization bugs uncovered by tests.
89
+ ## 0.0.2 (2013-09-30)
114
90
 
115
- * Fixed: Added a workaround for a possible spec bug when an `:at_keyword` is
116
- encountered while consuming declarations.
91
+ - Fixed: `:at_rule` nodes now have a `:name` key.
117
92
 
93
+ ## 0.0.1 (2013-09-27)
118
94
 
119
- 0.0.2 (2013-09-30)
120
- ------------------
121
-
122
- * Fixed: `:at_rule` nodes now have a `:name` key.
123
-
124
-
125
- 0.0.1 (2013-09-27)
126
- ------------------
127
-
128
- * Initial release.
95
+ - Initial release.
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2020 Ryan Grove (ryan@wonko.com)
1
+ Copyright Ryan Grove <ryan@wonko.com>
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy of
4
4
  this software and associated documentation files (the ‘Software’), to deal in
data/README.md CHANGED
@@ -1,64 +1,47 @@
1
- Crass
2
- =====
1
+ # Crass
3
2
 
4
- Crass is a Ruby CSS parser that's fully compliant with the
5
- [CSS Syntax Level 3][css] specification.
3
+ Crass is a Ruby CSS parser that's fully compliant with the [CSS Syntax Level 3](http://dev.w3.org/csswg/css-syntax/) specification.
6
4
 
7
- * [Home](https://github.com/rgrove/crass/)
8
- * [API Docs](http://rubydoc.info/github/rgrove/crass/master)
9
-
10
- [![Build Status](https://travis-ci.org/rgrove/crass.svg?branch=master)](https://travis-ci.org/rgrove/crass)
11
5
  [![Gem Version](https://badge.fury.io/rb/crass.svg)](http://badge.fury.io/rb/crass)
6
+ [![Tests](https://github.com/rgrove/crass/actions/workflows/tests.yml/badge.svg)](https://github.com/rgrove/crass/actions/workflows/tests.yml)
7
+
8
+ ## Links
9
+
10
+ - [Home](https://github.com/rgrove/crass/)
11
+ - [API Docs](https://rubydoc.info/github/rgrove/crass/Crass)
12
12
 
13
- Features
14
- --------
15
13
 
16
- * Pure Ruby, with no runtime dependencies other than Ruby 1.9.x or higher.
14
+ ## Features
17
15
 
18
- * Tokenizes and parses CSS according to the rules defined in the 14 November
19
- 2014 editor's draft of the [CSS Syntax Level 3][css] specification.
16
+ - Pure Ruby, with no runtime dependencies other than Ruby 1.9.x or higher.
20
17
 
21
- * Extremely tolerant of broken or invalid CSS. If a browser can handle it, Crass
22
- should be able to handle it too.
18
+ - Tokenizes and parses CSS according to the rules defined in the 14 November 2014 editor's draft of the [CSS Syntax Level 3](http://dev.w3.org/csswg/css-syntax/) specification.
23
19
 
24
- * Optionally includes comments in the token stream.
20
+ - Extremely tolerant of broken or invalid CSS. If a browser can handle it, Crass should be able to handle it too.
25
21
 
26
- * Optionally preserves certain CSS hacks, such as the IE "*" hack, which would
27
- otherwise be discarded according to CSS3 tokenizing rules.
22
+ - Optionally includes comments in the token stream.
28
23
 
29
- * Capable of serializing the parse tree back to CSS while maintaining all
30
- original whitespace, comments, and indentation.
24
+ - Optionally preserves certain CSS hacks, such as the IE "*" hack, which would otherwise be discarded according to CSS3 tokenizing rules.
31
25
 
32
- [css]: http://dev.w3.org/csswg/css-syntax/
26
+ - Capable of serializing the parse tree back to CSS while maintaining all original whitespace, comments, and indentation.
33
27
 
34
- Problems
35
- --------
28
+ ## Problems
36
29
 
37
- * Crass isn't terribly fast. I mean, it's Ruby, and it's not really slow by Ruby
38
- standards. But compared to the CSS parser in your average browser? Yeah, it's
39
- slow.
30
+ - Crass isn't terribly fast. I mean, it's Ruby, and it's not really slow by Ruby standards. But compared to the CSS parser in your average browser? Yeah, it's slow.
40
31
 
41
- * Crass only parses the CSS syntax; it doesn't understand what any of it means,
42
- doesn't coalesce selectors, etc. You can do this yourself by consuming the
43
- parse tree, though.
32
+ - Crass only parses the CSS syntax; it doesn't understand what any of it means, doesn't coalesce selectors, etc. You can do this yourself by consuming the parse tree, though.
44
33
 
45
- * While any node in the parse tree (or the parse tree as a whole) can be
46
- serialized back to CSS with perfect fidelity, changes made to those nodes
47
- (except for wholesale removal of nodes) are not reflected in the serialized
48
- output.
34
+ - While any node in the parse tree (or the parse tree as a whole) can be serialized back to CSS with perfect fidelity, changes made to those nodes (except for wholesale removal of nodes) are not reflected in the serialized output.
49
35
 
50
- * Crass only supports UTF-8 input and doesn't respect `@charset` rules. Input in
51
- any other encoding will be converted to UTF-8.
36
+ - Crass only supports UTF-8 input and doesn't respect `@charset` rules. Input in any other encoding will be converted to UTF-8.
52
37
 
53
- Installing
54
- ----------
38
+ ## Installing
55
39
 
56
40
  ```
57
41
  gem install crass
58
42
  ```
59
43
 
60
- Examples
61
- --------
44
+ ## Examples
62
45
 
63
46
  Say you have a string containing some CSS:
64
47
 
@@ -76,7 +59,7 @@ Parsing it is simple:
76
59
  tree = Crass.parse(css, :preserve_comments => true)
77
60
  ```
78
61
 
79
- This returns a big fat beautiful parse tree, which looks like this:
62
+ This returns a big beautiful parse tree, which looks like this:
80
63
 
81
64
  ```ruby
82
65
  [{:node=>:comment, :pos=>0, :raw=>"/* Comment! */", :value=>" Comment! "},
@@ -151,42 +134,20 @@ a:hover {
151
134
 
152
135
  Wasn't that exciting?
153
136
 
154
- A Note on Versioning
155
- --------------------
156
-
157
- As of version 1.0.0, Crass adheres strictly to [SemVer 2.0][semver].
158
-
159
- [semver]:http://semver.org/spec/v2.0.0.html
160
-
161
- Contributing
162
- ------------
137
+ ## Versioning
163
138
 
164
- The best way to contribute is to use Crass and [create issues][issue] when you
165
- run into problems.
139
+ As of version 1.0.0, Crass adheres strictly to [SemVer 2.0](http://semver.org/spec/v2.0.0.html).
166
140
 
167
- Pull requests that fix bugs are more than welcome as long as they include tests.
168
- Please adhere to the style and format of the surrounding code, or I might ask
169
- you to change things.
141
+ ## Contributing
170
142
 
171
- If you want to add a feature or refactor something, please get in touch first to
172
- make sure I'm on board with your idea and approach; I'm pretty picky, and I'd
173
- hate to have to turn down a pull request you spent a lot of time on.
143
+ The best way to contribute is to use Crass and [create issues](https://github.com/rgrove/crass/issues/new) when you run into problems.
174
144
 
175
- [issue]: https://github.com/rgrove/crass/issues/new
145
+ Pull requests that fix bugs are more than welcome as long as they include tests. Please adhere to the style and format of the surrounding code, or I might ask you to change things.
176
146
 
177
- Acknowledgments
178
- ---------------
147
+ If you want to add a feature or refactor something, please get in touch first to make sure I'm on board with your idea and approach; I'm pretty picky, and I'd hate to have to turn down a pull request you spent a lot of time on.
179
148
 
180
- I'm deeply, deeply grateful to [Simon Sapin][simon] for his wonderfully
181
- comprehensive [CSS parsing tests][css-tests], which I adapted to create many of
182
- Crass's tests. They've been invaluable in helping me fix bugs and handle weird
183
- edge cases, and Crass would be much crappier without them.
149
+ ## Acknowledgments
184
150
 
185
- I'm also grateful to [Tab Atkins Jr.][tab] and Simon Sapin (again!) for their
186
- work on the [CSS Syntax Level 3][spec] specification, which defines the
187
- tokenizing and parsing rules that Crass implements.
151
+ I'm deeply grateful to [Simon Sapin](http://exyr.org/about/) for his wonderfully comprehensive [CSS parsing tests](https://github.com/SimonSapin/css-parsing-tests/), which I adapted to create many of Crass's tests. They've been invaluable in helping me fix bugs and handle weird edge cases, and Crass would be much crappier without them.
188
152
 
189
- [css-tests]:https://github.com/SimonSapin/css-parsing-tests/
190
- [simon]:http://exyr.org/about/
191
- [spec]:http://www.w3.org/TR/css-syntax-3/
192
- [tab]:http://www.xanthir.com/contact/
153
+ I'm also grateful to [Tab Atkins-Bittner](https://www.xanthir.com/contact/) and Simon Sapin (again!) for their work on the [CSS Syntax Level 3](http://www.w3.org/TR/css-syntax-3/) specification, which defines the tokenizing and parsing rules that Crass implements.
data/SECURITY.md ADDED
@@ -0,0 +1,3 @@
1
+ # Security Policy
2
+
3
+ To report a security vulnerability in Crass, please email [ryan@wonko.com](mailto:ryan@wonko.com) or use GitHub's [Report a vulnerability](https://github.com/rgrove/crass/security/advisories/new) feature.
data/crass.gemspec CHANGED
@@ -2,20 +2,21 @@
2
2
  require './lib/crass/version'
3
3
 
4
4
  Gem::Specification.new do |s|
5
- s.name = 'crass'
6
- s.summary = 'CSS parser based on the CSS Syntax Level 3 spec.'
5
+ s.name = 'crass'
6
+ s.summary = 'CSS parser based on the CSS Syntax Level 3 spec.'
7
7
  s.description = 'Crass is a pure Ruby CSS parser based on the CSS Syntax Level 3 spec.'
8
- s.version = Crass::VERSION
9
- s.authors = ['Ryan Grove']
10
- s.email = ['ryan@wonko.com']
11
- s.homepage = 'https://github.com/rgrove/crass/'
12
- s.license = 'MIT'
8
+ s.version = Crass::VERSION
9
+ s.authors = ['Ryan Grove']
10
+ s.email = ['ryan@wonko.com']
11
+ s.homepage = 'https://github.com/rgrove/crass/'
12
+ s.license = 'MIT'
13
13
 
14
14
  s.metadata = {
15
- 'bug_tracker_uri' => 'https://github.com/rgrove/crass/issues',
16
- 'changelog_uri' => "https://github.com/rgrove/crass/blob/v#{s.version}/HISTORY.md",
15
+ 'bug_tracker_uri' => 'https://github.com/rgrove/crass/issues',
16
+ 'changelog_uri' => "https://github.com/rgrove/crass/blob/v#{s.version}/HISTORY.md",
17
17
  'documentation_uri' => "https://www.rubydoc.info/gems/crass/#{s.version}",
18
- 'source_code_uri' => "https://github.com/rgrove/crass/tree/v#{s.version}",
18
+ 'rubygems_mfa_required' => 'true',
19
+ 'source_code_uri' => "https://github.com/rgrove/crass/tree/v#{s.version}",
19
20
  }
20
21
 
21
22
  s.platform = Gem::Platform::RUBY
@@ -26,6 +27,6 @@ Gem::Specification.new do |s|
26
27
  s.files = `git ls-files -z`.split("\x0").grep_v(%r{^test/})
27
28
 
28
29
  # Development dependencies.
29
- s.add_development_dependency 'minitest', '~> 5.0.8'
30
- s.add_development_dependency 'rake', '~> 10.1.0'
30
+ s.add_development_dependency 'minitest', '~> 6.0.6'
31
+ s.add_development_dependency 'rake', '~> 13.4.2'
31
32
  end
data/lib/crass/parser.rb CHANGED
@@ -6,7 +6,7 @@ module Crass
6
6
 
7
7
  # Parses a CSS string or list of tokens.
8
8
  #
9
- # 5. http://dev.w3.org/csswg/css-syntax/#parsing
9
+ # 5. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#parsing
10
10
  class Parser
11
11
  BLOCK_END_TOKENS = {
12
12
  :'{' => :'}',
@@ -14,14 +14,28 @@ module Crass
14
14
  :'(' => :')'
15
15
  }
16
16
 
17
+ # Default maximum nesting depth for simple blocks and functions. This is far
18
+ # higher than any legitimate CSS needs, but far below the depth at which
19
+ # Ruby would raise `SystemStackError` while recursively parsing nested
20
+ # constructs.
21
+ #
22
+ # Keeping this low also bounds memory usage: each nested simple block and
23
+ # function retains a `:tokens` array spanning its descendants for
24
+ # serialization, so the total serialization metadata grows with nesting
25
+ # depth. A modest limit prevents deeply nested (but otherwise valid) input
26
+ # from amplifying memory disproportionately.
27
+ #
28
+ # It can be overridden with the `:maximum_depth` option.
29
+ DEFAULT_MAXIMUM_DEPTH = 25
30
+
17
31
  # -- Class Methods ---------------------------------------------------------
18
32
 
19
33
  # Parses CSS properties (such as the contents of an HTML element's `style`
20
34
  # attribute) and returns a parse tree.
21
35
  #
22
- # See {Tokenizer#initialize} for _options_.
36
+ # See {Crass.parse} for _options_.
23
37
  #
24
- # 5.3.6. http://dev.w3.org/csswg/css-syntax/#parse-a-list-of-declarations
38
+ # 5.3.6. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#parse-a-list-of-declarations
25
39
  def self.parse_properties(input, options = {})
26
40
  Parser.new(input, options).parse_properties
27
41
  end
@@ -30,9 +44,9 @@ module Crass
30
44
  # parse tree. The only difference from {parse_stylesheet} is that CDO/CDC
31
45
  # nodes (`<!--` and `-->`) aren't ignored.
32
46
  #
33
- # See {Tokenizer#initialize} for _options_.
47
+ # See {Crass.parse} for _options_.
34
48
  #
35
- # 5.3.3. http://dev.w3.org/csswg/css-syntax/#parse-a-list-of-rules
49
+ # 5.3.3. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#parse-a-list-of-rules
36
50
  def self.parse_rules(input, options = {})
37
51
  parser = Parser.new(input, options)
38
52
  rules = parser.consume_rules
@@ -48,9 +62,9 @@ module Crass
48
62
 
49
63
  # Parses a CSS stylesheet and returns a parse tree.
50
64
  #
51
- # See {Tokenizer#initialize} for _options_.
65
+ # See {Crass.parse} for _options_.
52
66
  #
53
- # 5.3.2. http://dev.w3.org/csswg/css-syntax/#parse-a-stylesheet
67
+ # 5.3.2. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#parse-a-stylesheet
54
68
  def self.parse_stylesheet(input, options = {})
55
69
  parser = Parser.new(input, options)
56
70
  rules = parser.consume_rules(:top_level => true)
@@ -122,18 +136,20 @@ module Crass
122
136
  # Initializes a parser based on the given _input_, which may be a CSS string
123
137
  # or an array of tokens.
124
138
  #
125
- # See {Tokenizer#initialize} for _options_.
139
+ # See {Crass.parse} for _options_.
126
140
  def initialize(input, options = {})
127
141
  unless input.kind_of?(Enumerable)
128
142
  input = Tokenizer.tokenize(input, options)
129
143
  end
130
144
 
145
+ @depth = 0
146
+ @maximum_depth = options[:maximum_depth] || DEFAULT_MAXIMUM_DEPTH
131
147
  @tokens = TokenScanner.new(input)
132
148
  end
133
149
 
134
150
  # Consumes an at-rule and returns it.
135
151
  #
136
- # 5.4.2. http://dev.w3.org/csswg/css-syntax-3/#consume-at-rule
152
+ # 5.4.2. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-at-rule
137
153
  def consume_at_rule(input = @tokens)
138
154
  rule = {}
139
155
 
@@ -180,7 +196,7 @@ module Crass
180
196
  # Consumes a component value and returns it, or `nil` if there are no more
181
197
  # tokens.
182
198
  #
183
- # 5.4.6. http://dev.w3.org/csswg/css-syntax-3/#consume-a-component-value
199
+ # 5.4.6. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-a-component-value
184
200
  def consume_component_value(input = @tokens)
185
201
  return nil unless token = input.consume
186
202
 
@@ -205,7 +221,7 @@ module Crass
205
221
 
206
222
  # Consumes a declaration and returns it, or `nil` on parse error.
207
223
  #
208
- # 5.4.5. http://dev.w3.org/csswg/css-syntax-3/#consume-a-declaration
224
+ # 5.4.5. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-a-declaration
209
225
  def consume_declaration(input = @tokens)
210
226
  declaration = {}
211
227
  value = []
@@ -272,7 +288,7 @@ module Crass
272
288
  # * **:strict** - Set to `true` to exclude non-standard `:comment`,
273
289
  # `:semicolon`, and `:whitespace` nodes.
274
290
  #
275
- # 5.4.4. http://dev.w3.org/csswg/css-syntax/#consume-a-list-of-declarations
291
+ # 5.4.4. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-a-list-of-declarations
276
292
  def consume_declarations(input = @tokens, options = {})
277
293
  declarations = []
278
294
 
@@ -322,38 +338,48 @@ module Crass
322
338
 
323
339
  # Consumes a function and returns it.
324
340
  #
325
- # 5.4.8. http://dev.w3.org/csswg/css-syntax-3/#consume-a-function
341
+ # 5.4.8. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-a-function
326
342
  def consume_function(input = @tokens)
327
- function = {
328
- :name => input.current[:value],
329
- :value => [],
330
- :tokens => [input.current] # Non-standard, used for serialization.
331
- }
332
-
333
- function[:tokens].concat(input.collect {
334
- while token = input.consume
335
- case token[:node]
336
- when :')'
337
- break
338
-
339
- # Non-standard.
340
- when :comment
341
- next
342
-
343
- else
344
- input.reconsume
345
- function[:value] << consume_component_value(input)
343
+ @depth += 1
344
+
345
+ begin
346
+ # Discard functions nested more deeply than the maximum allowed depth to
347
+ # avoid exhausting the Ruby stack on maliciously nested input.
348
+ return discard_block(input) if @depth > @maximum_depth
349
+
350
+ function = {
351
+ :name => input.current[:value],
352
+ :value => [],
353
+ :tokens => [input.current] # Non-standard, used for serialization.
354
+ }
355
+
356
+ function[:tokens].concat(input.collect {
357
+ while token = input.consume
358
+ case token[:node]
359
+ when :')'
360
+ break
361
+
362
+ # Non-standard.
363
+ when :comment
364
+ next
365
+
366
+ else
367
+ input.reconsume
368
+ function[:value] << consume_component_value(input)
369
+ end
346
370
  end
347
- end
348
- })
371
+ })
349
372
 
350
- create_node(:function, function)
373
+ create_node(:function, function)
374
+ ensure
375
+ @depth -= 1
376
+ end
351
377
  end
352
378
 
353
379
  # Consumes a qualified rule and returns it, or `nil` if a parse error
354
380
  # occurs.
355
381
  #
356
- # 5.4.3. http://dev.w3.org/csswg/css-syntax-3/#consume-a-qualified-rule
382
+ # 5.4.3. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-a-qualified-rule
357
383
  def consume_qualified_rule(input = @tokens)
358
384
  rule = {:prelude => []}
359
385
 
@@ -394,7 +420,7 @@ module Crass
394
420
 
395
421
  # Consumes a list of rules and returns them.
396
422
  #
397
- # 5.4.1. http://dev.w3.org/csswg/css-syntax/#consume-a-list-of-rules
423
+ # 5.4.1. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-a-list-of-rules
398
424
  def consume_rules(flags = {})
399
425
  rules = []
400
426
 
@@ -430,28 +456,70 @@ module Crass
430
456
  # Consumes and returns a simple block associated with the current input
431
457
  # token.
432
458
  #
433
- # 5.4.7. http://dev.w3.org/csswg/css-syntax/#consume-a-simple-block
459
+ # 5.4.7. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-a-simple-block
434
460
  def consume_simple_block(input = @tokens)
435
- start_token = input.current[:node]
436
- end_token = BLOCK_END_TOKENS[start_token]
461
+ @depth += 1
437
462
 
438
- block = {
439
- :start => start_token.to_s,
440
- :end => end_token.to_s,
441
- :value => [],
442
- :tokens => [input.current] # Non-standard. Used for serialization.
443
- }
463
+ begin
464
+ # Discard blocks nested more deeply than the maximum allowed depth to
465
+ # avoid exhausting the Ruby stack on maliciously nested input.
466
+ return discard_block(input) if @depth > @maximum_depth
444
467
 
445
- block[:tokens].concat(input.collect do
446
- while token = input.consume
447
- break if token[:node] == end_token
468
+ start_token = input.current[:node]
469
+ end_token = BLOCK_END_TOKENS[start_token]
448
470
 
449
- input.reconsume
450
- block[:value] << consume_component_value(input)
471
+ block = {
472
+ :start => start_token.to_s,
473
+ :end => end_token.to_s,
474
+ :value => [],
475
+ :tokens => [input.current] # Non-standard. Used for serialization.
476
+ }
477
+
478
+ block[:tokens].concat(input.collect do
479
+ while token = input.consume
480
+ break if token[:node] == end_token
481
+
482
+ input.reconsume
483
+ block[:value] << consume_component_value(input)
484
+ end
485
+ end)
486
+
487
+ create_node(:simple_block, block)
488
+ ensure
489
+ @depth -= 1
490
+ end
491
+ end
492
+
493
+ # Discards an over-nested simple block or function without recursing, then
494
+ # returns an `:error` node. Assumes `input.current` is the opening token (a
495
+ # `{`, `[`, `(`, or function token).
496
+ #
497
+ # This is reached only when the configured maximum nesting depth is
498
+ # exceeded. It iteratively consumes tokens up to the matching closing token
499
+ # (tracking nested blocks and functions with an explicit stack) so that a
500
+ # deeply nested construct can't exhaust the Ruby stack.
501
+ def discard_block(input)
502
+ opening = input.current
503
+
504
+ stack = [opening[:node] == :function ? :')' : BLOCK_END_TOKENS[opening[:node]]]
505
+ tokens = [opening] # Non-standard. Used for serialization.
506
+
507
+ tokens.concat(input.collect do
508
+ until stack.empty?
509
+ break unless token = input.consume
510
+
511
+ case token[:node]
512
+ when :'{', :'[', :'('
513
+ stack.push(BLOCK_END_TOKENS[token[:node]])
514
+ when :function
515
+ stack.push(:')')
516
+ when stack.last
517
+ stack.pop
518
+ end
451
519
  end
452
520
  end)
453
521
 
454
- create_node(:simple_block, block)
522
+ create_node(:error, :value => 'maximum-depth-exceeded', :tokens => tokens)
455
523
  end
456
524
 
457
525
  # Creates and returns a new parse node with the given _properties_.
@@ -479,7 +547,7 @@ module Crass
479
547
 
480
548
  # Parses a single component value and returns it.
481
549
  #
482
- # 5.3.7. http://dev.w3.org/csswg/css-syntax-3/#parse-a-component-value
550
+ # 5.3.7. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#parse-a-component-value
483
551
  def parse_component_value(input = @tokens)
484
552
  input = TokenScanner.new(input) unless input.is_a?(TokenScanner)
485
553
 
@@ -506,7 +574,7 @@ module Crass
506
574
 
507
575
  # Parses a list of component values and returns an array of parsed tokens.
508
576
  #
509
- # 5.3.8. http://dev.w3.org/csswg/css-syntax/#parse-a-list-of-component-values
577
+ # 5.3.8. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#parse-a-list-of-component-values
510
578
  def parse_component_values(input = @tokens)
511
579
  input = TokenScanner.new(input) unless input.is_a?(TokenScanner)
512
580
  tokens = []
@@ -520,7 +588,7 @@ module Crass
520
588
 
521
589
  # Parses a single declaration and returns it.
522
590
  #
523
- # 5.3.5. http://dev.w3.org/csswg/css-syntax/#parse-a-declaration
591
+ # 5.3.5. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#parse-a-declaration
524
592
  def parse_declaration(input = @tokens)
525
593
  input = TokenScanner.new(input) unless input.is_a?(TokenScanner)
526
594
 
@@ -548,7 +616,7 @@ module Crass
548
616
  #
549
617
  # See {#consume_declarations} for _options_.
550
618
  #
551
- # 5.3.6. http://dev.w3.org/csswg/css-syntax/#parse-a-list-of-declarations
619
+ # 5.3.6. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#parse-a-list-of-declarations
552
620
  def parse_declarations(input = @tokens, options = {})
553
621
  input = TokenScanner.new(input) unless input.is_a?(TokenScanner)
554
622
  consume_declarations(input, options)
@@ -582,7 +650,7 @@ module Crass
582
650
 
583
651
  # Parses a single rule and returns it.
584
652
  #
585
- # 5.3.4. http://dev.w3.org/csswg/css-syntax-3/#parse-a-rule
653
+ # 5.3.4. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#parse-a-rule
586
654
  def parse_rule(input = @tokens)
587
655
  input = TokenScanner.new(input) unless input.is_a?(TokenScanner)
588
656
 
data/lib/crass/scanner.rb CHANGED
@@ -16,6 +16,10 @@ module Crass
16
16
 
17
17
  # Position of the next character that will be consumed. This is a character
18
18
  # position, not a byte position, so it accounts for multi-byte characters.
19
+ #
20
+ # Byte offsets (used internally for fast substring extraction) are tracked
21
+ # separately by the underlying StringScanner, whose `pos` always reflects
22
+ # the byte offset corresponding to this character position.
19
23
  attr_accessor :pos
20
24
 
21
25
  # String being scanned.
@@ -46,6 +50,11 @@ module Crass
46
50
  def consume_rest
47
51
  result = @scanner.rest
48
52
 
53
+ # `StringScanner#rest` does not advance the scan pointer, so move it to
54
+ # the end of the input to keep the byte offset in sync with {#pos}. This
55
+ # ensures a subsequent {#marked} extracts the correct substring.
56
+ @scanner.terminate
57
+
49
58
  @current = result[-1]
50
59
  @pos = @len
51
60
 
@@ -61,24 +70,31 @@ module Crass
61
70
  # Sets the marker to the position of the next character that will be
62
71
  # consumed.
63
72
  def mark
64
- @marker = @pos
73
+ @byte_marker = @scanner.pos
74
+ @marker = @pos
65
75
  end
66
76
 
67
77
  # Returns the substring between {#marker} and {#pos}, without altering the
68
78
  # pointer.
69
79
  def marked
70
- if result = @string[@marker, @pos - @marker]
71
- result
72
- else
73
- ''
74
- end
80
+ # Extract the marked text using byte offsets rather than character
81
+ # offsets. Slicing the original string by character offset is O(n) on
82
+ # multi-byte input (Ruby must translate the character index into a byte
83
+ # index), which makes tokenizing non-ASCII input superlinear. Byte slicing
84
+ # is O(length) regardless of how far into the string we are.
85
+ @string.byteslice(@byte_marker, @scanner.pos - @byte_marker) || ''
75
86
  end
76
87
 
77
88
  # Returns up to _length_ characters starting at the current position, but
78
89
  # doesn't consume them. The number of characters returned may be less than
79
90
  # _length_ if the end of the string is reached.
80
91
  def peek(length = 1)
81
- @string[pos, length]
92
+ # Grab the bytes for up to _length_ characters and then take the first
93
+ # _length_ characters. A UTF-8 character is at most four bytes, so `length
94
+ # * 4` bytes always contains at least _length_ whole characters when that
95
+ # many remain. This avoids the O(n) character-offset slice that
96
+ # `@string[pos, length]` would otherwise perform on multi-byte input.
97
+ @string.byteslice(@scanner.pos, length * 4).slice(0, length) || ''
82
98
  end
83
99
 
84
100
  # Moves the pointer back one character without changing the value of
@@ -91,10 +107,13 @@ module Crass
91
107
 
92
108
  # Resets the pointer to the beginning of the string.
93
109
  def reset
94
- @current = nil
95
- @len = @string.size
96
- @marker = 0
97
- @pos = 0
110
+ @scanner.reset
111
+
112
+ @byte_marker = 0
113
+ @current = nil
114
+ @len = @string.size
115
+ @marker = 0
116
+ @pos = 0
98
117
  end
99
118
 
100
119
  # Tries to match _pattern_ at the current position. If it matches, the
@@ -5,7 +5,7 @@ module Crass
5
5
 
6
6
  # Tokenizes a CSS string.
7
7
  #
8
- # 4. http://dev.w3.org/csswg/css-syntax/#tokenization
8
+ # 4. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#tokenization
9
9
  class Tokenizer
10
10
  RE_COMMENT_CLOSE = /\*\//
11
11
  RE_DIGIT = /[0-9]+/
@@ -66,19 +66,20 @@ module Crass
66
66
 
67
67
  # Consumes a token and returns the token that was consumed.
68
68
  #
69
- # 4.3.1. http://dev.w3.org/csswg/css-syntax/#consume-a-token
69
+ # 4.3.1. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-a-token
70
70
  def consume
71
- return nil if @s.eos?
71
+ # Skip over any comments. When comments aren't being preserved this is
72
+ # done iteratively rather than recursively so that a long run of adjacent
73
+ # comments can't exhaust the Ruby stack and raise a `SystemStackError`.
74
+ loop do
75
+ return nil if @s.eos?
72
76
 
73
- @s.mark
77
+ @s.mark
74
78
 
75
- # Consume comments.
76
- if comment_token = consume_comments
77
- if @options[:preserve_comments]
78
- return comment_token
79
- else
80
- return consume
81
- end
79
+ comment_token = consume_comments
80
+ break unless comment_token
81
+
82
+ return comment_token if @options[:preserve_comments]
82
83
  end
83
84
 
84
85
  # Consume whitespace.
@@ -88,7 +89,7 @@ module Crass
88
89
 
89
90
  case char.to_sym
90
91
  when :'"'
91
- consume_string
92
+ consume_string(char)
92
93
 
93
94
  when :'#'
94
95
  if @s.peek =~ RE_NAME || valid_escape?(@s.peek(2))
@@ -108,7 +109,7 @@ module Crass
108
109
  end
109
110
 
110
111
  when :"'"
111
- consume_string
112
+ consume_string(char)
112
113
 
113
114
  when :'('
114
115
  create_token(:'(')
@@ -131,7 +132,7 @@ module Crass
131
132
  end
132
133
 
133
134
  when :+
134
- if start_number?
135
+ if start_number?(char + @s.peek(2))
135
136
  @s.reconsume
136
137
  consume_numeric
137
138
  else
@@ -160,7 +161,7 @@ module Crass
160
161
  end
161
162
 
162
163
  when :'.'
163
- if start_number?
164
+ if start_number?(char + @s.peek(2))
164
165
  @s.reconsume
165
166
  consume_numeric
166
167
  else
@@ -195,7 +196,7 @@ module Crass
195
196
  create_token(:'[')
196
197
 
197
198
  when :'\\'
198
- if valid_escape?
199
+ if valid_escape?(char + @s.peek)
199
200
  @s.reconsume
200
201
  consume_ident
201
202
  else
@@ -271,14 +272,12 @@ module Crass
271
272
 
272
273
  # Consumes the remnants of a bad URL and returns the consumed text.
273
274
  #
274
- # 4.3.15. http://dev.w3.org/csswg/css-syntax/#consume-the-remnants-of-a-bad-url
275
+ # 4.3.15. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-the-remnants-of-a-bad-url
275
276
  def consume_bad_url
276
277
  text = String.new
277
278
 
278
279
  until @s.eos?
279
- if valid_escape?
280
- text << consume_escaped
281
- elsif valid_escape?(@s.peek(2))
280
+ if valid_escape?(@s.peek(2))
282
281
  @s.consume
283
282
  text << consume_escaped
284
283
  else
@@ -297,7 +296,7 @@ module Crass
297
296
 
298
297
  # Consumes comments and returns them, or `nil` if no comments were consumed.
299
298
  #
300
- # 4.3.2. http://dev.w3.org/csswg/css-syntax/#consume-comments
299
+ # 4.3.2. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-comments
301
300
  def consume_comments
302
301
  if @s.peek(2) == '/*'
303
302
  @s.consume
@@ -319,10 +318,10 @@ module Crass
319
318
  # Consumes an escaped code point and returns its unescaped value.
320
319
  #
321
320
  # This method assumes that the `\` has already been consumed, and that the
322
- # next character in the input has already been verified not to be a newline
323
- # or EOF.
321
+ # next character in the input has already been verified not to be a
322
+ # newline. If the input is at EOF, U+FFFD is returned.
324
323
  #
325
- # 4.3.8. http://dev.w3.org/csswg/css-syntax/#consume-an-escaped-code-point
324
+ # 4.3.8. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-an-escaped-code-point
326
325
  def consume_escaped
327
326
  return "\ufffd" if @s.eos?
328
327
 
@@ -346,7 +345,7 @@ module Crass
346
345
 
347
346
  # Consumes an ident-like token and returns it.
348
347
  #
349
- # 4.3.4. http://dev.w3.org/csswg/css-syntax/#consume-an-ident-like-token
348
+ # 4.3.4. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-an-ident-like-token
350
349
  def consume_ident
351
350
  value = consume_name
352
351
 
@@ -371,7 +370,7 @@ module Crass
371
370
 
372
371
  # Consumes a name and returns it.
373
372
  #
374
- # 4.3.12. http://dev.w3.org/csswg/css-syntax/#consume-a-name
373
+ # 4.3.12. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-a-name
375
374
  def consume_name
376
375
  result = String.new
377
376
 
@@ -383,7 +382,7 @@ module Crass
383
382
 
384
383
  char = @s.consume
385
384
 
386
- if valid_escape?
385
+ if valid_escape?(char + @s.peek)
387
386
  result << consume_escaped
388
387
 
389
388
  # Non-standard: IE * hack
@@ -403,7 +402,7 @@ module Crass
403
402
  # original representation, its numeric value, and its type (either
404
403
  # `:integer` or `:number`).
405
404
  #
406
- # 4.3.13. http://dev.w3.org/csswg/css-syntax/#consume-a-number
405
+ # 4.3.13. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-a-number
407
406
  def consume_number
408
407
  repr = String.new
409
408
  type = :integer
@@ -426,7 +425,7 @@ module Crass
426
425
 
427
426
  # Consumes a numeric token and returns it.
428
427
  #
429
- # 4.3.3. http://dev.w3.org/csswg/css-syntax/#consume-a-numeric-token
428
+ # 4.3.3. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-a-numeric-token
430
429
  def consume_numeric
431
430
  number = consume_number
432
431
  repr = number[0]
@@ -465,7 +464,7 @@ module Crass
465
464
  # Consumes a string token that ends at the given character, and returns the
466
465
  # token.
467
466
  #
468
- # 4.3.5. http://dev.w3.org/csswg/css-syntax/#consume-a-string-token
467
+ # 4.3.5. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-a-string-token
469
468
  def consume_string(ending = nil)
470
469
  ending = @s.current if ending.nil?
471
470
  value = String.new
@@ -506,7 +505,7 @@ module Crass
506
505
  # Consumes a Unicode range token and returns it. Assumes the initial "u+" or
507
506
  # "U+" has already been consumed.
508
507
  #
509
- # 4.3.7. http://dev.w3.org/csswg/css-syntax/#consume-a-unicode-range-token
508
+ # 4.3.7. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-a-unicode-range-token
510
509
  def consume_unicode_range
511
510
  value = @s.scan(RE_HEX) || String.new
512
511
 
@@ -538,7 +537,7 @@ module Crass
538
537
  # Consumes a URL token and returns it. Assumes the original "url(" has
539
538
  # already been consumed.
540
539
  #
541
- # 4.3.6. http://dev.w3.org/csswg/css-syntax/#consume-a-url-token
540
+ # 4.3.6. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-a-url-token
542
541
  def consume_url
543
542
  value = String.new
544
543
 
@@ -566,7 +565,7 @@ module Crass
566
565
  :value => value + consume_bad_url)
567
566
 
568
567
  when '\\'
569
- if valid_escape?
568
+ if valid_escape?(char + @s.peek)
570
569
  value << consume_escaped
571
570
  else
572
571
  # Parse error.
@@ -586,7 +585,7 @@ module Crass
586
585
 
587
586
  # Converts a valid CSS number string into a number and returns the number.
588
587
  #
589
- # 4.3.14. http://dev.w3.org/csswg/css-syntax/#convert-a-string-to-a-number
588
+ # 4.3.14. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#convert-a-string-to-a-number
590
589
  def convert_string_to_number(str)
591
590
  matches = RE_NUMBER_STR.match(str)
592
591
 
@@ -597,16 +596,42 @@ module Crass
597
596
  t = matches[:exponent_sign] == '-' ? -1 : 1
598
597
  e = matches[:exponent].to_i
599
598
 
600
- # I know this formula looks nutty, but it's exactly what's defined in the
601
- # spec, and it works.
602
- value = s * (i + f * 10**-d) * 10**(t * e)
599
+ exponent = t * e
600
+
601
+ # Guard against denial of service via attacker-controlled exponentiation.
602
+ # A tiny input like "1e5000000" would otherwise force Ruby to compute
603
+ # `10**exponent` (an enormous integer) before the value is clamped below,
604
+ # consuming disproportionate CPU and memory. When the exponent's magnitude
605
+ # is far outside the range a finite double can represent, we saturate or
606
+ # underflow without performing the exponentiation.
607
+ if i == 0 && f == 0
608
+ # The mantissa is zero, so the value is zero regardless of the exponent.
609
+ value = s * 0.0
610
+
611
+ elsif exponent - d > Float::MAX_10_EXP + 1
612
+ # The value is larger than `Float::MAX` and would be clamped anyway.
613
+ value = s * Float::MAX
614
+
615
+ elsif exponent + matches[:integer].length < -325
616
+ # The value is smaller than the smallest representable double and would
617
+ # round to zero. (-325 is one less than the smallest subnormal base-10
618
+ # exponent of roughly -324; `matches[:integer].length` is an upper bound
619
+ # on the mantissa's magnitude, ensuring we never zero a representable
620
+ # value.)
621
+ value = s * 0.0
603
622
 
604
- # Maximum and minimum values aren't defined in the spec, but are enforced
605
- # here for sanity.
606
- if value > Float::MAX
607
- value = Float::MAX
608
- elsif value < -Float::MAX
609
- value = -Float::MAX
623
+ else
624
+ # I know this formula looks nutty, but it's exactly what's defined in
625
+ # the spec, and it works.
626
+ value = s * (i + f * 10**-d) * 10**exponent
627
+
628
+ # Maximum and minimum values aren't defined in the spec, but are
629
+ # enforced here for sanity.
630
+ if value > Float::MAX
631
+ value = Float::MAX
632
+ elsif value < -Float::MAX
633
+ value = -Float::MAX
634
+ end
610
635
  end
611
636
 
612
637
  value
@@ -623,7 +648,7 @@ module Crass
623
648
 
624
649
  # Preprocesses _input_ to prepare it for the tokenizer.
625
650
  #
626
- # 3.3. http://dev.w3.org/csswg/css-syntax/#input-preprocessing
651
+ # 3.3. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#input-preprocessing
627
652
  def preprocess(input)
628
653
  input = input.to_s.encode('UTF-8',
629
654
  :invalid => :replace,
@@ -638,7 +663,7 @@ module Crass
638
663
  # identifier. If _text_ is `nil`, the current and next two characters in the
639
664
  # input stream will be checked, but will not be consumed.
640
665
  #
641
- # 4.3.10. http://dev.w3.org/csswg/css-syntax/#would-start-an-identifier
666
+ # 4.3.10. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#would-start-an-identifier
642
667
  def start_identifier?(text = nil)
643
668
  text = @s.current + @s.peek(2) if text.nil?
644
669
 
@@ -662,7 +687,7 @@ module Crass
662
687
  # If _text_ is `nil`, the current and next two characters in the input
663
688
  # stream will be checked, but will not be consumed.
664
689
  #
665
- # 4.3.11. http://dev.w3.org/csswg/css-syntax/#starts-with-a-number
690
+ # 4.3.11. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#starts-with-a-number
666
691
  def start_number?(text = nil)
667
692
  text = @s.current + @s.peek(2) if text.nil?
668
693
 
@@ -698,7 +723,7 @@ module Crass
698
723
  # valid escape sequence. If _text_ is `nil`, the current and next character
699
724
  # in the input stream will be checked, but will not be consumed.
700
725
  #
701
- # 4.3.9. http://dev.w3.org/csswg/css-syntax/#starts-with-a-valid-escape
726
+ # 4.3.9. https://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#starts-with-a-valid-escape
702
727
  def valid_escape?(text = nil)
703
728
  text = @s.current + @s.peek if text.nil?
704
729
  !!(text[0] == '\\' && text[1] != "\n")
data/lib/crass/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module Crass
4
- VERSION = '1.0.6'
4
+ VERSION = '1.0.8'
5
5
  end
data/lib/crass.rb CHANGED
@@ -6,7 +6,19 @@ module Crass
6
6
 
7
7
  # Parses _input_ as a CSS stylesheet and returns a parse tree.
8
8
  #
9
- # See {Tokenizer#initialize} for _options_.
9
+ # Options:
10
+ #
11
+ # * **:maximum_depth** - Maximum nesting depth for simple blocks and
12
+ # functions. Constructs nested more deeply than this are discarded to
13
+ # prevent stack exhaustion. Defaults to {Parser::DEFAULT_MAXIMUM_DEPTH}.
14
+ #
15
+ # * **:preserve_comments** - If `true`, comments will be preserved as
16
+ # `:comment` tokens.
17
+ #
18
+ # * **:preserve_hacks** - If `true`, certain non-standard browser hacks
19
+ # such as the IE "*" hack will be preserved even though they violate
20
+ # CSS 3 syntax rules.
21
+ #
10
22
  def self.parse(input, options = {})
11
23
  Parser.parse_stylesheet(input, options)
12
24
  end
@@ -14,7 +26,7 @@ module Crass
14
26
  # Parses _input_ as a string of CSS properties (such as the contents of an
15
27
  # HTML element's `style` attribute) and returns a parse tree.
16
28
  #
17
- # See {Tokenizer#initialize} for _options_.
29
+ # See {Crass.parse} for _options_.
18
30
  def self.parse_properties(input, options = {})
19
31
  Parser.parse_properties(input, options)
20
32
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: crass
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.6
4
+ version: 1.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Grove
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2020-01-12 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: minitest
@@ -16,28 +15,28 @@ dependencies:
16
15
  requirements:
17
16
  - - "~>"
18
17
  - !ruby/object:Gem::Version
19
- version: 5.0.8
18
+ version: 6.0.6
20
19
  type: :development
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - "~>"
25
24
  - !ruby/object:Gem::Version
26
- version: 5.0.8
25
+ version: 6.0.6
27
26
  - !ruby/object:Gem::Dependency
28
27
  name: rake
29
28
  requirement: !ruby/object:Gem::Requirement
30
29
  requirements:
31
30
  - - "~>"
32
31
  - !ruby/object:Gem::Version
33
- version: 10.1.0
32
+ version: 13.4.2
34
33
  type: :development
35
34
  prerelease: false
36
35
  version_requirements: !ruby/object:Gem::Requirement
37
36
  requirements:
38
37
  - - "~>"
39
38
  - !ruby/object:Gem::Version
40
- version: 10.1.0
39
+ version: 13.4.2
41
40
  description: Crass is a pure Ruby CSS parser based on the CSS Syntax Level 3 spec.
42
41
  email:
43
42
  - ryan@wonko.com
@@ -45,14 +44,15 @@ executables: []
45
44
  extensions: []
46
45
  extra_rdoc_files: []
47
46
  files:
47
+ - ".github/workflows/tests.yml"
48
48
  - ".gitignore"
49
- - ".travis.yml"
50
49
  - ".yardopts"
51
50
  - Gemfile
52
51
  - HISTORY.md
53
52
  - LICENSE
54
53
  - README.md
55
54
  - Rakefile
55
+ - SECURITY.md
56
56
  - crass.gemspec
57
57
  - lib/crass.rb
58
58
  - lib/crass/parser.rb
@@ -65,10 +65,10 @@ licenses:
65
65
  - MIT
66
66
  metadata:
67
67
  bug_tracker_uri: https://github.com/rgrove/crass/issues
68
- changelog_uri: https://github.com/rgrove/crass/blob/v1.0.6/HISTORY.md
69
- documentation_uri: https://www.rubydoc.info/gems/crass/1.0.6
70
- source_code_uri: https://github.com/rgrove/crass/tree/v1.0.6
71
- post_install_message:
68
+ changelog_uri: https://github.com/rgrove/crass/blob/v1.0.8/HISTORY.md
69
+ documentation_uri: https://www.rubydoc.info/gems/crass/1.0.8
70
+ rubygems_mfa_required: 'true'
71
+ source_code_uri: https://github.com/rgrove/crass/tree/v1.0.8
72
72
  rdoc_options: []
73
73
  require_paths:
74
74
  - lib
@@ -83,8 +83,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
83
83
  - !ruby/object:Gem::Version
84
84
  version: '0'
85
85
  requirements: []
86
- rubygems_version: 3.0.3
87
- signing_key:
86
+ rubygems_version: 4.0.20
88
87
  specification_version: 4
89
88
  summary: CSS parser based on the CSS Syntax Level 3 spec.
90
89
  test_files: []
data/.travis.yml DELETED
@@ -1,11 +0,0 @@
1
- language: ruby
2
- rvm:
3
- - 2.3
4
- - 2.4
5
- - 2.5
6
- - 2.6
7
- - ruby-head
8
-
9
- matrix:
10
- allow_failures:
11
- - rvm: ruby-head