token-resolver 1.0.0 → 1.0.1

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: 9aafcdcd6d3f7a05af0fbb5c0a59c9e146e6dae06167d58377e4a45cb7413411
4
- data.tar.gz: 1cc7453a96d191312242f4534cc49601e2df38c6121a5b75b88fa999a3c6936c
3
+ metadata.gz: 66d9dd5b6e50ded563ba876174ef20a039d7c074193136becdb5c49117b34039
4
+ data.tar.gz: 24245d7bd943145c2c7096931d3d2b5c514b226538e2c1f0f35dc408c72a9cb8
5
5
  SHA512:
6
- metadata.gz: 7406a221baac8a84c873818ea242f38622d3290fe2b51cc252f1653d396225c89d6e1bc1571911e38092b035a7269db1cf22568a5e6fb0b4eda3935db8614cfa
7
- data.tar.gz: 3db97ee6943027bf55cf212a572f4c7f73de2018073f5262dc601b17c75fab5fc1d0aed0b1488176464a04ad9fa17bf4ca2d817b3d5bd2e128ab8ec5b8ffcd5f
6
+ metadata.gz: 178cc7d07c311b8c8c66b022c71a6ef0e6fa6e903b00abe2c815d217ff03129ff546b66ea02f956e85c71149a56c02340e4f0fdff7c6acb02fe44e6d736736cb
7
+ data.tar.gz: 9acb5dd248d648b4db87d34fa39f34401e365904b899736a0ef97f21dca47beb8446b02673e9ce7dd7bdca5045a909127b2765e18bc99a5a8ffd8d079cfd917e
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,33 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [1.0.1] - 2026-02-22
34
+
35
+ - TAG: [v1.0.1][1.0.1t]
36
+ - COVERAGE: 98.13% -- 263/268 lines in 10 files
37
+ - BRANCH COVERAGE: 91.18% -- 62/68 branches in 10 files
38
+ - 96.77% documented
39
+
40
+ ### Added
41
+
42
+ - `Config#segment_pattern` option — a parslet character class constraining which characters
43
+ are valid inside token segments (default: `"[A-Za-z0-9_]"`). This prevents false positive
44
+ token matches against Ruby block parameters (`{ |x| expr }`), shell variable expansion
45
+ (`${VAR:+val}`), and other syntax that structurally resembles tokens but contains spaces
46
+ or punctuation in the "segments".
47
+ - `Resolve#resolve` now validates replacement keys against the config's `segment_pattern` and
48
+ raises `ArgumentError` if a key contains characters that the grammar would never parse.
49
+
50
+ ### Fixed
51
+
52
+ - **False positive token matches** — the grammar previously used `any` (match any character)
53
+ for segment content, which allowed spaces, operators, and punctuation inside token segments.
54
+ This caused Ruby block syntax like `{ |fp| File.exist?(fp) }` and shell expansion like
55
+ `${CLASSPATH:+:$CLASSPATH}` to be incorrectly parsed as tokens. With multi-separator configs
56
+ (`["|", ":"]`), the second `|` was reconstructed as `:` during `on_missing: :keep`
57
+ roundtripping, silently corrupting source files. The grammar now uses
58
+ `match(segment_pattern)` instead of `any`, limiting segments to word characters by default.
59
+
33
60
  ## [1.0.0] - 2026-02-21
34
61
 
35
62
  - TAG: [v1.0.0][1.0.0t]
@@ -43,6 +70,8 @@ Please file a bug if you notice a violation of semantic versioning.
43
70
 
44
71
  ### Security
45
72
 
46
- [Unreleased]: https://github.com/kettle-rb/token-resolver/compare/v1.0.0...HEAD
73
+ [Unreleased]: https://github.com/kettle-rb/token-resolver/compare/v1.0.1...HEAD
74
+ [1.0.1]: https://github.com/kettle-rb/token-resolver/compare/v1.0.0...v1.0.1
75
+ [1.0.1t]: https://github.com/kettle-rb/token-resolver/releases/tag/v1.0.1
47
76
  [1.0.0]: https://github.com/kettle-rb/ast-merge/compare/e0e299cad6e6914d512845c71df6b7ac8009e5ac...v1.0.0
48
77
  [1.0.0t]: https://github.com/kettle-rb/ast-merge/tags/v1.0.0
data/README.md CHANGED
@@ -157,13 +157,36 @@ NOTE: Be prepared to track down certs for signed gems and add them the same way
157
157
 
158
158
  ### Token Config Options
159
159
 
160
- | Option | Default | Description |
161
- |--------|---------|-------------|
162
- | `pre` | `"{"` | Opening delimiter |
163
- | `post` | `"}"` | Closing delimiter |
164
- | `separators` | `["\|"]` | Segment separators (sequential; last repeats) |
165
- | `min_segments` | `2` | Minimum segments for a valid token |
166
- | `max_segments` | `nil` | Maximum segments (`nil` = unlimited) |
160
+ | Option | Default | Description |
161
+ |-------------------|---------------------|----------------------------------------------------|
162
+ | `pre` | `"{"` | Opening delimiter |
163
+ | `post` | `"}"` | Closing delimiter |
164
+ | `separators` | `["|"]` (pipe) | Segment separators (sequential; last repeats) |
165
+ | `min_segments` | `2` | Minimum segments for a valid token |
166
+ | `max_segments` | `nil` | Maximum segments (`nil` = unlimited) |
167
+ | `segment_pattern` | `"[A-Za-z0-9_]"` | Parslet character class for valid segment content |
168
+
169
+ ### Segment Character Constraints
170
+
171
+ Token segments (the parts between delimiters and separators) only match characters that
172
+ conform to the `segment_pattern`. By default, this is word characters: uppercase and
173
+ lowercase letters, digits, and underscores.
174
+
175
+ This prevents false positives with syntax that structurally resembles tokens but isn't:
176
+
177
+ ```ruby
178
+ # These are NOT parsed as tokens (spaces, punctuation disqualify them):
179
+ "items.map { |x| x.to_s }" # Ruby block parameters
180
+ "${CLASSPATH:+:$CLASSPATH}" # Shell variable expansion
181
+ "cert_chain.select! { |fp| File.exist? }" # Ruby block with expressions
182
+ ```
183
+
184
+ If you need different characters in your token segments, provide a custom pattern:
185
+
186
+ ```ruby
187
+ # Allow hyphens in segments: {NS|my-key}
188
+ config = Token::Resolver::Config.new(segment_pattern: "[A-Za-z0-9_-]")
189
+ ```
167
190
 
168
191
  ## 🔧 Basic Usage
169
192
 
@@ -263,6 +286,14 @@ infinite loops and ensures predictable behavior when replacement values contain
263
286
  If the input doesn't contain the `pre` delimiter at all, the parser fast-paths and returns
264
287
  a single Text node without invoking parslet.
265
288
 
289
+ ### False Positive Prevention
290
+
291
+ The grammar constrains segment content to the configured `segment_pattern` (default: word
292
+ characters). This ensures that syntax using the same delimiter characters — such as Ruby
293
+ block parameters (`{ |x| expr }`) or shell variable expansion (`${VAR:+val}`) — is never
294
+ mistakenly parsed as a token. Replacement keys that contain characters outside the
295
+ `segment_pattern` are rejected with an `ArgumentError` at resolve time.
296
+
266
297
 
267
298
  ## 🦷 FLOSS Funding
268
299
 
@@ -623,7 +654,7 @@ Thanks for RTFM. ☺️
623
654
  [📌gitmoji]: https://gitmoji.dev
624
655
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
625
656
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
626
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-0.258-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
657
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-0.268-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
627
658
  [🔐security]: SECURITY.md
628
659
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
629
660
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -44,6 +44,11 @@ module Token
44
44
  # @return [Integer, nil] Maximum number of segments (nil = unlimited)
45
45
  attr_reader :max_segments
46
46
 
47
+ # @return [String] Parslet-compatible character class for segment content.
48
+ # Only characters matching this pattern are valid inside a token segment.
49
+ # Default: `"[A-Za-z0-9_]"` (word characters — no spaces, no operators).
50
+ attr_reader :segment_pattern
51
+
47
52
  # Create a new Config.
48
53
  #
49
54
  # @param pre [String] Opening delimiter (default: "{")
@@ -51,9 +56,11 @@ module Token
51
56
  # @param separators [Array<String>] Segment separators (default: ["|"])
52
57
  # @param min_segments [Integer] Minimum segment count (default: 2)
53
58
  # @param max_segments [Integer, nil] Maximum segment count (default: nil)
59
+ # @param segment_pattern [String] Parslet match() character class for valid segment
60
+ # characters (default: "[A-Za-z0-9_]")
54
61
  #
55
62
  # @raise [ArgumentError] If any delimiter is empty or constraints are invalid
56
- def initialize(pre: "{", post: "}", separators: ["|"], min_segments: 2, max_segments: nil)
63
+ def initialize(pre: "{", post: "}", separators: ["|"], min_segments: 2, max_segments: nil, segment_pattern: "[A-Za-z0-9_]")
57
64
  validate!(pre, post, separators, min_segments, max_segments)
58
65
 
59
66
  @pre = pre.dup.freeze
@@ -61,6 +68,7 @@ module Token
61
68
  @separators = separators.map { |s| s.dup.freeze }.freeze
62
69
  @min_segments = min_segments
63
70
  @max_segments = max_segments
71
+ @segment_pattern = segment_pattern.dup.freeze
64
72
 
65
73
  freeze
66
74
  end
@@ -85,7 +93,8 @@ module Token
85
93
  post == other.post &&
86
94
  separators == other.separators &&
87
95
  min_segments == other.min_segments &&
88
- max_segments == other.max_segments
96
+ max_segments == other.max_segments &&
97
+ segment_pattern == other.segment_pattern
89
98
  end
90
99
 
91
100
  alias_method :==, :eql?
@@ -94,7 +103,7 @@ module Token
94
103
  #
95
104
  # @return [Integer]
96
105
  def hash
97
- [pre, post, separators, min_segments, max_segments].hash
106
+ [pre, post, separators, min_segments, max_segments, segment_pattern].hash
98
107
  end
99
108
 
100
109
  # Get the separator for a given boundary index.
@@ -53,6 +53,7 @@ module Token
53
53
  separators = config.separators
54
54
  min_segs = config.min_segments
55
55
  max_segs = config.max_segments
56
+ seg_pattern = config.segment_pattern
56
57
 
57
58
  Class.new(Parslet::Parser) do
58
59
  # A segment is one or more characters that are not a separator or post delimiter.
@@ -62,10 +63,11 @@ module Token
62
63
  # Build the set of strings that terminate a segment
63
64
  terminators = ([post_str] + separators).uniq
64
65
 
65
- # segment: one or more chars that aren't any terminator
66
+ # segment: one or more chars that match the segment_pattern and
67
+ # aren't any terminator string.
66
68
  rule(:segment) {
67
69
  terminator_absent = terminators.map { |t| str(t).absent? }.reduce(:>>)
68
- (terminator_absent >> any).repeat(1)
70
+ (terminator_absent >> match(seg_pattern)).repeat(1)
69
71
  }
70
72
 
71
73
  # token: pre + segment + (sep + segment).repeat + post
@@ -50,16 +50,19 @@ module Token
50
50
  # @return [String] Resolved text
51
51
  #
52
52
  # @raise [UnresolvedTokenError] If on_missing is :raise and a token has no replacement
53
+ # @raise [ArgumentError] If a replacement key contains characters outside the config's segment_pattern
53
54
  def resolve(document_or_nodes, replacements)
54
- nodes = case document_or_nodes
55
+ nodes, config = case document_or_nodes
55
56
  when Document
56
- document_or_nodes.nodes
57
+ [document_or_nodes.nodes, document_or_nodes.config]
57
58
  when Array
58
- document_or_nodes
59
+ [document_or_nodes, nil]
59
60
  else
60
61
  raise ArgumentError, "Expected Document or Array of nodes, got #{document_or_nodes.class}"
61
62
  end
62
63
 
64
+ validate_replacement_keys!(replacements, config) if config && !replacements.empty?
65
+
63
66
  result = +""
64
67
  nodes.each do |node|
65
68
  if node.token?
@@ -88,6 +91,28 @@ module Token
88
91
  # emit nothing
89
92
  end
90
93
  end
94
+
95
+ # Validate that all replacement keys only contain characters allowed by the config.
96
+ # Each key is composed of segments (matching segment_pattern) joined by separators.
97
+ #
98
+ # @param replacements [Hash{String => String}]
99
+ # @param config [Config]
100
+ # @raise [ArgumentError] If any key contains invalid characters
101
+ def validate_replacement_keys!(replacements, config)
102
+ # Build a regex that matches a valid key: segment (sep segment)*
103
+ seg = config.segment_pattern
104
+ seps = config.separators.map { |s| Regexp.escape(s) }.join("|")
105
+ valid_key_re = /\A#{seg}+((?:#{seps})#{seg}+)*\z/
106
+
107
+ replacements.each_key do |key|
108
+ unless valid_key_re.match?(key)
109
+ raise ArgumentError,
110
+ "Invalid replacement key: #{key.inspect}. " \
111
+ "Key segments must match #{config.segment_pattern.inspect} " \
112
+ "and be separated by one of #{config.separators.inspect}."
113
+ end
114
+ end
115
+ end
91
116
  end
92
117
  end
93
118
  end
@@ -5,7 +5,7 @@ module Token
5
5
  # Version information for Token::Resolver
6
6
  module Version
7
7
  # Current version of the token-resolver gem
8
- VERSION = "1.0.0"
8
+ VERSION = "1.0.1"
9
9
  end
10
10
  VERSION = Version::VERSION # traditional location
11
11
  end
data.tar.gz.sig CHANGED
@@ -1,2 +1,4 @@
1
- ?<"�˽��(���S��wzӈ�_B�+7�i�H�nxv���ۨ��t"��Y9��u��0Ti��P]/�Z�03�vh���ڸ�B�8*�v�@�
2
- �v-�Y5[Q�3fG�)8T{)+����C[2������ъ�ߌ �q��a��_��9���,��:�D�6���A����i�5i�@��Uv��e�?�����d��$�Bí�%y ���d�2/9�
1
+ iد�쾰�'Ctۨfi��(�t!��IS���?��o�
2
+ }l���1�%��p=>U
3
+ �#F��h<cg0G��,* A�4)8��h�h`]��X�ۛ��M�<���-M�!���Z�txKFl�CJ�ˇ�d^ � �6�;+�ԝ�Q3�m�{���X
4
+ ���ѫ1ى�p����[z{v�r&2�� N�,���9�k��Q攛�Տݰr졽�^��"uQI ���+r����F��Vm��Uxw&`� ��Rނ��������[]Z��x����j���b
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: token-resolver
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -274,10 +274,10 @@ licenses:
274
274
  - MIT
275
275
  metadata:
276
276
  homepage_uri: https://token-resolver.galtzo.com/
277
- source_code_uri: https://github.com/kettle-rb/token-resolver/tree/v1.0.0
278
- changelog_uri: https://github.com/kettle-rb/token-resolver/blob/v1.0.0/CHANGELOG.md
277
+ source_code_uri: https://github.com/kettle-rb/token-resolver/tree/v1.0.1
278
+ changelog_uri: https://github.com/kettle-rb/token-resolver/blob/v1.0.1/CHANGELOG.md
279
279
  bug_tracker_uri: https://github.com/kettle-rb/token-resolver/issues
280
- documentation_uri: https://www.rubydoc.info/gems/token-resolver/1.0.0
280
+ documentation_uri: https://www.rubydoc.info/gems/token-resolver/1.0.1
281
281
  funding_uri: https://github.com/sponsors/pboling
282
282
  wiki_uri: https://github.com/kettle-rb/token-resolver/wiki
283
283
  news_uri: https://www.railsbling.com/tags/token-resolver
metadata.gz.sig CHANGED
Binary file