otto 2.0.0 → 2.0.2

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: 8539a9a6889a7976aa6ceb87ea3d7d489cb82b65e283cb57d7d731e2f2247159
4
- data.tar.gz: d5c025e966063e8f1eb618c1a6aa28782db50e7f55769f322b557a0be4725d4b
3
+ metadata.gz: fabc1e8d6f7eef1d982b1f298fd0fe68382417aedd4b250ded28e5c7ba39a588
4
+ data.tar.gz: 02f4509daad92151895100c75339d82e3f8b76106dbcaa04b2db34f7b4d39976
5
5
  SHA512:
6
- metadata.gz: 06dc04e8f9ada60160c87c48c32a31e34fbab8b29a0f1755fa9f7bf2878aebe7ea1491110018cfcdc2e689be8de41ec17ae2f413cc0f38ff9136c0d039104f78
7
- data.tar.gz: c12c1236b295148a9f1f1b31f7fb22aeace22494703e49ec4c47421d7455f74ea0ebcb017019ebe799b5b930cd4a907d5bf6b7ed3f6e58bcac7920432d7ada90
6
+ metadata.gz: 2c5f1958418f70a429bfc2379f7407387ca4a1a6cdd129160ff6bf3e416f33a637c0d14049bd97ab0c7abce02ab5b11eb943aff330b5c1c0425d55c387ff3351
7
+ data.tar.gz: '08e5ec5c3278adeced0dd3c1cffb7452129a12ad742c381b29a29bd569be4d013a67a660ad111bfe51631672e21d96cf2c0f39c69ab595758aa0e7e1d562ed32'
@@ -22,18 +22,45 @@ jobs:
22
22
  test:
23
23
  timeout-minutes: 10
24
24
  runs-on: ubuntu-24.04
25
- name: "RSpec Tests (Ruby ${{ matrix.ruby }})"
25
+ name: "RSpec Tests (Ruby ${{ matrix.ruby }}, ${{ matrix.lockfile }})"
26
26
  continue-on-error: ${{ matrix.experimental }}
27
27
  strategy:
28
28
  fail-fast: false
29
29
  matrix:
30
+ # Each Ruby runs twice: once against the committed Gemfile.lock
31
+ # (floor of the declared version range, reproducible) and once
32
+ # with the lockfile removed so Bundler resolves fresh inside the
33
+ # gemspec's pessimistic constraints (ceiling, what a downstream
34
+ # user will actually hit). The unlocked cells catch upstream
35
+ # releases that satisfy `~> X.Y` but break Otto at load time -
36
+ # e.g. facets 3.2.0 shipping a self-referential
37
+ # `require_relative 'file/write.rb'` against a file deleted in
38
+ # the same release, the reason 2.0.2 exists.
30
39
  include:
31
40
  - ruby: "3.3"
32
41
  experimental: false
42
+ lockfile: "locked"
33
43
  - ruby: "3.4"
34
44
  experimental: false
45
+ lockfile: "locked"
35
46
  - ruby: "3.5"
36
47
  experimental: true
48
+ lockfile: "locked"
49
+ - ruby: "4.0"
50
+ experimental: true
51
+ lockfile: "locked"
52
+ - ruby: "3.3"
53
+ experimental: false
54
+ lockfile: "unlocked"
55
+ - ruby: "3.4"
56
+ experimental: false
57
+ lockfile: "unlocked"
58
+ - ruby: "3.5"
59
+ experimental: true
60
+ lockfile: "unlocked"
61
+ - ruby: "4.0"
62
+ experimental: true
63
+ lockfile: "unlocked"
37
64
 
38
65
  steps:
39
66
  - uses: actions/checkout@v6
@@ -42,7 +69,9 @@ jobs:
42
69
  continue-on-error: ${{ matrix.experimental }}
43
70
  with:
44
71
  ruby-version: ${{ matrix.ruby }}
45
- bundler-cache: ${{ !matrix.experimental }}
72
+ # Bundler cache keys off Gemfile.lock, so only enable it on the
73
+ # locked matrix cells. Unlocked cells need a fresh resolve each run.
74
+ bundler-cache: ${{ !matrix.experimental && matrix.lockfile == 'locked' }}
46
75
 
47
76
  - name: Setup tmate session
48
77
  uses: mxschmitt/action-tmate@c0afd6f790e3a5564914980036ebf83216678101 # v3
@@ -52,9 +81,25 @@ jobs:
52
81
 
53
82
  - name: Install dependencies
54
83
  continue-on-error: ${{ matrix.experimental }}
84
+ env:
85
+ EXPERIMENTAL: ${{ matrix.experimental }}
86
+ LOCKFILE: ${{ matrix.lockfile }}
55
87
  run: |
56
88
  bundle config path vendor/bundle
57
- bundle install --jobs 4 --retry 3 --with test
89
+ # Enable the optional :development, :test group (json_schemer,
90
+ # rack-attack) that specs depend on. `bundle install --with` was
91
+ # removed in Bundler 2.7, which ships with Ruby 4.0.
92
+ bundle config set --local with 'development test'
93
+ if [ "$EXPERIMENTAL" = "true" ]; then
94
+ bundle config set --local force_ruby_platform true
95
+ fi
96
+ if [ "$LOCKFILE" = "unlocked" ]; then
97
+ # Drop the committed lockfile so Bundler resolves fresh inside
98
+ # the gemspec's pessimistic constraints. This surfaces the gap
99
+ # between "version range we declare" and "version range we test."
100
+ rm -f Gemfile.lock
101
+ fi
102
+ bundle install --jobs 4 --retry 3
58
103
 
59
104
  - name: Verify setup
60
105
  run: |
data/CHANGELOG.rst CHANGED
@@ -7,6 +7,34 @@ The format is based on `Keep a Changelog <https://keepachangelog.com/en/1.1.0/>`
7
7
 
8
8
  <!--scriv-insert-here-->
9
9
 
10
+ .. _changelog-2.0.2:
11
+
12
+ 2.0.2 — 2026-04-15
13
+ ==================
14
+
15
+ - Load failure under facets 3.2.0. ``Otto::Security::ValidationHelpers`` no
16
+ longer requires ``facets/file``, whose aggregator in 3.2.0 does
17
+ ``require_relative 'file/write.rb'`` against a file deleted in the same
18
+ release. The one function Otto borrowed from facets — ``File.sanitize`` —
19
+ is now inlined as a private method on the helper module (with credit in
20
+ the source comment), and the ``facets`` runtime dependency is removed
21
+ from the gemspec entirely. Applications depending on facets directly are
22
+ unaffected.
23
+
24
+ - CI now runs the RSpec suite twice for each Ruby in the matrix: once
25
+ against the committed ``Gemfile.lock`` and once with the lockfile removed
26
+ so Bundler resolves fresh inside the gemspec's pessimistic constraints.
27
+ The unlocked cells catch upstream releases that satisfy ``~> X.Y`` but
28
+ break Otto at load time.
29
+
30
+ .. _changelog-2.0.1:
31
+
32
+ 2.0.1 — 2026-04-15
33
+ ==================
34
+
35
+ - Allow running with Ruby 4
36
+ - Update gems rack, ruby-lsp, rspec, rubocop, loofah, rack-test
37
+
10
38
  .. _changelog-2.0.0:
11
39
 
12
40
  2.0.0 — 2026-03-14
data/Gemfile CHANGED
@@ -9,8 +9,6 @@ source 'https://rubygems.org'
9
9
 
10
10
  gemspec
11
11
 
12
- gem 'rackup'
13
-
14
12
  group :test do
15
13
  gem 'rack-test'
16
14
  gem 'rspec', '~> 3.13'
@@ -28,6 +26,7 @@ end
28
26
  group :development do
29
27
  gem 'benchmark'
30
28
  gem 'debug'
29
+ gem 'rackup' # Used to boot examples/ apps; not needed by specs
31
30
  gem 'rubocop', '~> 1.81.7', require: false
32
31
  gem 'rubocop-performance', require: false
33
32
  gem 'rubocop-rspec', require: false
data/Gemfile.lock CHANGED
@@ -1,9 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- otto (2.0.0)
4
+ otto (2.0.2)
5
5
  concurrent-ruby (~> 1.3, < 2.0)
6
- facets (~> 3.1)
7
6
  ipaddr (~> 1, < 2.0)
8
7
  logger (~> 1, < 2.0)
9
8
  loofah (~> 2.20)
@@ -16,8 +15,8 @@ GEM
16
15
  specs:
17
16
  ast (2.4.3)
18
17
  benchmark (0.5.0)
19
- bigdecimal (3.3.1)
20
- concurrent-ruby (1.3.5)
18
+ bigdecimal (4.1.1)
19
+ concurrent-ruby (1.3.6)
21
20
  crass (1.0.6)
22
21
  date (3.4.1)
23
22
  debug (1.11.0)
@@ -46,23 +45,22 @@ GEM
46
45
  dry-logic (~> 1.5)
47
46
  dry-types (~> 1.8)
48
47
  zeitwerk (~> 2.6)
49
- dry-types (1.8.3)
50
- bigdecimal (~> 3.0)
48
+ dry-types (1.9.1)
49
+ bigdecimal (>= 3.0)
51
50
  concurrent-ruby (~> 1.0)
52
51
  dry-core (~> 1.0)
53
52
  dry-inflector (~> 1.0)
54
53
  dry-logic (~> 1.4)
55
54
  zeitwerk (~> 2.6)
56
55
  erb (5.1.1)
57
- facets (3.1.0)
58
56
  hana (1.3.7)
59
57
  io-console (0.8.1)
60
- ipaddr (1.2.7)
58
+ ipaddr (1.2.8)
61
59
  irb (1.15.2)
62
60
  pp (>= 0.6.0)
63
61
  rdoc (>= 4.0.0)
64
62
  reline (>= 0.4.2)
65
- json (2.16.0)
63
+ json (2.19.3)
66
64
  json_schemer (2.5.0)
67
65
  bigdecimal
68
66
  hana (~> 1.3)
@@ -71,28 +69,28 @@ GEM
71
69
  language_server-protocol (3.17.0.5)
72
70
  lint_roller (1.1.0)
73
71
  logger (1.7.0)
74
- loofah (2.24.1)
72
+ loofah (2.25.1)
75
73
  crass (~> 1.0.2)
76
74
  nokogiri (>= 1.12.0)
77
75
  minitest (5.26.0)
78
- nokogiri (1.18.10-aarch64-linux-gnu)
76
+ nokogiri (1.19.2-aarch64-linux-gnu)
79
77
  racc (~> 1.4)
80
- nokogiri (1.18.10-aarch64-linux-musl)
78
+ nokogiri (1.19.2-aarch64-linux-musl)
81
79
  racc (~> 1.4)
82
- nokogiri (1.18.10-arm-linux-gnu)
80
+ nokogiri (1.19.2-arm-linux-gnu)
83
81
  racc (~> 1.4)
84
- nokogiri (1.18.10-arm-linux-musl)
82
+ nokogiri (1.19.2-arm-linux-musl)
85
83
  racc (~> 1.4)
86
- nokogiri (1.18.10-arm64-darwin)
84
+ nokogiri (1.19.2-arm64-darwin)
87
85
  racc (~> 1.4)
88
- nokogiri (1.18.10-x86_64-darwin)
86
+ nokogiri (1.19.2-x86_64-darwin)
89
87
  racc (~> 1.4)
90
- nokogiri (1.18.10-x86_64-linux-gnu)
88
+ nokogiri (1.19.2-x86_64-linux-gnu)
91
89
  racc (~> 1.4)
92
- nokogiri (1.18.10-x86_64-linux-musl)
90
+ nokogiri (1.19.2-x86_64-linux-musl)
93
91
  racc (~> 1.4)
94
- parallel (1.27.0)
95
- parser (3.3.10.0)
92
+ parallel (1.28.0)
93
+ parser (3.3.11.1)
96
94
  ast (~> 2.4.1)
97
95
  racc
98
96
  pastel (0.8.0)
@@ -101,12 +99,12 @@ GEM
101
99
  prettyprint
102
100
  prettier_print (1.2.1)
103
101
  prettyprint (0.2.0)
104
- prism (1.6.0)
102
+ prism (1.9.0)
105
103
  psych (5.2.6)
106
104
  date
107
105
  stringio
108
106
  racc (1.8.1)
109
- rack (3.2.4)
107
+ rack (3.2.6)
110
108
  rack-attack (6.8.0)
111
109
  rack (>= 1.0, < 4)
112
110
  rack-parser (0.7.0)
@@ -116,8 +114,10 @@ GEM
116
114
  rackup (2.3.1)
117
115
  rack (>= 3)
118
116
  rainbow (3.1.1)
119
- rbs (3.9.5)
117
+ rbs (4.0.2)
120
118
  logger
119
+ prism (>= 1.6.0)
120
+ tsort
121
121
  rdoc (6.15.0)
122
122
  erb
123
123
  psych (>= 4.0.0)
@@ -128,7 +128,7 @@ GEM
128
128
  parser (~> 3.3.0)
129
129
  rainbow (>= 2.0, < 4.0)
130
130
  rexml (~> 3.1)
131
- regexp_parser (2.11.3)
131
+ regexp_parser (2.12.0)
132
132
  reline (0.6.2)
133
133
  io-console (~> 0.5)
134
134
  rexml (3.4.4)
@@ -141,10 +141,10 @@ GEM
141
141
  rspec-expectations (3.13.5)
142
142
  diff-lcs (>= 1.2.0, < 2.0)
143
143
  rspec-support (~> 3.13.0)
144
- rspec-mocks (3.13.6)
144
+ rspec-mocks (3.13.8)
145
145
  diff-lcs (>= 1.2.0, < 2.0)
146
146
  rspec-support (~> 3.13.0)
147
- rspec-support (3.13.6)
147
+ rspec-support (3.13.7)
148
148
  rubocop (1.81.7)
149
149
  json (~> 2.3)
150
150
  language_server-protocol (~> 3.17.0.2)
@@ -156,21 +156,21 @@ GEM
156
156
  rubocop-ast (>= 1.47.1, < 2.0)
157
157
  ruby-progressbar (~> 1.7)
158
158
  unicode-display_width (>= 2.4.0, < 4.0)
159
- rubocop-ast (1.48.0)
159
+ rubocop-ast (1.49.1)
160
160
  parser (>= 3.3.7.2)
161
- prism (~> 1.4)
161
+ prism (~> 1.7)
162
162
  rubocop-performance (1.26.1)
163
163
  lint_roller (~> 1.1)
164
164
  rubocop (>= 1.75.0, < 2.0)
165
165
  rubocop-ast (>= 1.47.1, < 2.0)
166
- rubocop-rspec (3.8.0)
166
+ rubocop-rspec (3.9.0)
167
167
  lint_roller (~> 1.1)
168
168
  rubocop (~> 1.81)
169
169
  rubocop-thread_safety (0.7.3)
170
170
  lint_roller (~> 1.1)
171
171
  rubocop (~> 1.72, >= 1.72.1)
172
172
  rubocop-ast (>= 1.44.0, < 2.0)
173
- ruby-lsp (0.26.4)
173
+ ruby-lsp (0.26.9)
174
174
  language_server-protocol (~> 3.17.0)
175
175
  prism (>= 1.2, < 2.0)
176
176
  rbs (>= 3, < 5)
@@ -195,8 +195,8 @@ GEM
195
195
  tty-screen (0.8.2)
196
196
  unicode-display_width (3.2.0)
197
197
  unicode-emoji (~> 4.1)
198
- unicode-emoji (4.1.0)
199
- user_agent_parser (2.20.0)
198
+ unicode-emoji (4.2.0)
199
+ user_agent_parser (2.21.0)
200
200
  zeitwerk (2.7.3)
201
201
 
202
202
  PLATFORMS
@@ -3,12 +3,20 @@
3
3
  # frozen_string_literal: true
4
4
 
5
5
  require 'loofah'
6
- require 'facets/file'
7
6
 
8
7
  class Otto
9
8
  module Security
10
9
  # Validation helper methods providing input validation and sanitization
11
10
  module ValidationHelpers
11
+ # Replace filesystem-unsafe characters with an underscore. Borrowed
12
+ # verbatim from facets 3.1.0's `File.sanitize` (lib/core/facets/file/
13
+ # sanitize.rb, credit: George Moschovitis) and inlined here so Otto
14
+ # doesn't take a runtime dep on the whole facets grab-bag for one
15
+ # 12-line function. See commit message for 2.0.2 for context.
16
+ FILENAME_SANITIZE_PATTERN = /[^a-zA-Z0-9.\-+_]/
17
+ FILENAME_DOT_ONLY = /^\.+$/
18
+ private_constant :FILENAME_SANITIZE_PATTERN, :FILENAME_DOT_ONLY
19
+
12
20
  def validate_input(input, max_length: 1000, allow_html: false)
13
21
  return input if input.nil?
14
22
 
@@ -42,20 +50,16 @@ class Otto
42
50
  return nil if filename.nil?
43
51
  return 'file' if filename.empty?
44
52
 
45
- # Use Facets File.sanitize for basic filesystem-safe filename
46
- clean_name = File.sanitize(filename.to_s)
53
+ clean_name = basic_filename_sanitize(filename.to_s)
47
54
 
48
- # Handle edge cases and improve on Facets behavior to match test expectations
49
55
  if clean_name.nil? || clean_name.empty?
50
56
  clean_name = 'file'
51
57
  else
52
- # Additional cleanup that Facets doesn't do but our tests expect
53
- clean_name = clean_name.gsub(/_{2,}/, '_') # Collapse multiple underscores
54
- clean_name = clean_name.gsub(/^_+|_+$/, '') # Remove leading/trailing underscores
55
- clean_name = 'file' if clean_name.empty? # Handle case where only underscores remain
58
+ clean_name = clean_name.gsub(/_{2,}/, '_')
59
+ clean_name = clean_name.gsub(/^_+|_+$/, '')
60
+ clean_name = 'file' if clean_name.empty? || clean_name.match?(FILENAME_DOT_ONLY)
56
61
  end
57
62
 
58
- # Ensure reasonable length (255 is filesystem limit, leave some padding)
59
63
  clean_name = clean_name[0..99] if clean_name.length > 100
60
64
 
61
65
  clean_name
@@ -63,6 +67,17 @@ class Otto
63
67
 
64
68
  private
65
69
 
70
+ # Filesystem-safe basename. Port of facets 3.1.0's `File.sanitize`:
71
+ # strip directory components (handling backslashes for IE-uploaded
72
+ # paths), replace anything outside [A-Za-z0-9.\-+_] with '_', and
73
+ # prefix a leading '_' if the whole name is just dots ('.', '..').
74
+ def basic_filename_sanitize(filename)
75
+ name = File.basename(filename.gsub('\\', '/'))
76
+ name = name.gsub(FILENAME_SANITIZE_PATTERN, '_')
77
+ name = "_#{name}" if name.match?(FILENAME_DOT_ONLY)
78
+ name
79
+ end
80
+
66
81
  # Check if content looks like it contains HTML tags or entities
67
82
  def contains_html_like_content?(content)
68
83
  content.match?(/[<>&]/) || content.match?(/&\w+;/)
data/lib/otto/version.rb CHANGED
@@ -3,5 +3,5 @@
3
3
  # frozen_string_literal: true
4
4
 
5
5
  class Otto
6
- VERSION = '2.0.0'
6
+ VERSION = '2.0.2'
7
7
  end
data/otto.gemspec CHANGED
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
18
18
  spec.homepage = 'https://github.com/delano/otto'
19
19
  spec.require_paths = ['lib']
20
20
 
21
- spec.required_ruby_version = ['>= 3.2', '< 4.0']
21
+ spec.required_ruby_version = ['>= 3.2', '< 4.1']
22
22
 
23
23
  spec.add_dependency 'concurrent-ruby', '~> 1.3', '< 2.0'
24
24
  spec.add_dependency 'ipaddr', '~> 1', '< 2.0'
@@ -31,7 +31,6 @@ Gem::Specification.new do |spec|
31
31
  spec.add_dependency 'rexml', '~> 3.4'
32
32
 
33
33
  # Security dependencies
34
- spec.add_dependency 'facets', '~> 3.1'
35
34
  spec.add_dependency 'loofah', '~> 2.20'
36
35
 
37
36
  # Optional MCP dependencies
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: otto
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Delano Mandelbaum
@@ -117,20 +117,6 @@ dependencies:
117
117
  - - "~>"
118
118
  - !ruby/object:Gem::Version
119
119
  version: '3.4'
120
- - !ruby/object:Gem::Dependency
121
- name: facets
122
- requirement: !ruby/object:Gem::Requirement
123
- requirements:
124
- - - "~>"
125
- - !ruby/object:Gem::Version
126
- version: '3.1'
127
- type: :runtime
128
- prerelease: false
129
- version_requirements: !ruby/object:Gem::Requirement
130
- requirements:
131
- - - "~>"
132
- - !ruby/object:Gem::Version
133
- version: '3.1'
134
120
  - !ruby/object:Gem::Dependency
135
121
  name: loofah
136
122
  requirement: !ruby/object:Gem::Requirement
@@ -337,14 +323,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
337
323
  version: '3.2'
338
324
  - - "<"
339
325
  - !ruby/object:Gem::Version
340
- version: '4.0'
326
+ version: '4.1'
341
327
  required_rubygems_version: !ruby/object:Gem::Requirement
342
328
  requirements:
343
329
  - - ">="
344
330
  - !ruby/object:Gem::Version
345
331
  version: '0'
346
332
  requirements: []
347
- rubygems_version: 3.7.2
333
+ rubygems_version: 3.6.9
348
334
  specification_version: 4
349
335
  summary: Define your rack-apps in plaintext.
350
336
  test_files: []