otto 2.0.1 → 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: 3f3bb450ec316b56ed419765cc0453d9a1b00584a071e77ee03ec14b2deea1f7
4
- data.tar.gz: 5a9f16d21067b4ce3287902c4da6342820414d1f8156470e410ed74ad672faef
3
+ metadata.gz: fabc1e8d6f7eef1d982b1f298fd0fe68382417aedd4b250ded28e5c7ba39a588
4
+ data.tar.gz: 02f4509daad92151895100c75339d82e3f8b76106dbcaa04b2db34f7b4d39976
5
5
  SHA512:
6
- metadata.gz: 98313ac7cdf3f6f00fbe2d93422c2f464c45663bb52135c4baeb85a61cd33d90702394ba50284f84099cfce1f6e32bcf6605520fe38434653ded9c762e94ac15
7
- data.tar.gz: b81b1c05ea9258f3d57286411f926959562a78daf377e8fad924de83d376e2fd6d8f342bd175b4b0d135b5e57321775d354c1b8bd06a08d13dae011686e4e4ac
6
+ metadata.gz: 2c5f1958418f70a429bfc2379f7407387ca4a1a6cdd129160ff6bf3e416f33a637c0d14049bd97ab0c7abce02ab5b11eb943aff330b5c1c0425d55c387ff3351
7
+ data.tar.gz: '08e5ec5c3278adeced0dd3c1cffb7452129a12ad742c381b29a29bd569be4d013a67a660ad111bfe51631672e21d96cf2c0f39c69ab595758aa0e7e1d562ed32'
@@ -22,20 +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"
37
49
  - ruby: "4.0"
38
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"
39
64
 
40
65
  steps:
41
66
  - uses: actions/checkout@v6
@@ -44,7 +69,9 @@ jobs:
44
69
  continue-on-error: ${{ matrix.experimental }}
45
70
  with:
46
71
  ruby-version: ${{ matrix.ruby }}
47
- 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' }}
48
75
 
49
76
  - name: Setup tmate session
50
77
  uses: mxschmitt/action-tmate@c0afd6f790e3a5564914980036ebf83216678101 # v3
@@ -56,12 +83,23 @@ jobs:
56
83
  continue-on-error: ${{ matrix.experimental }}
57
84
  env:
58
85
  EXPERIMENTAL: ${{ matrix.experimental }}
86
+ LOCKFILE: ${{ matrix.lockfile }}
59
87
  run: |
60
88
  bundle config path vendor/bundle
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'
61
93
  if [ "$EXPERIMENTAL" = "true" ]; then
62
94
  bundle config set --local force_ruby_platform true
63
95
  fi
64
- bundle install --jobs 4 --retry 3 --with test
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
65
103
 
66
104
  - name: Verify setup
67
105
  run: |
data/CHANGELOG.rst CHANGED
@@ -7,6 +7,26 @@ 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
+
10
30
  .. _changelog-2.0.1:
11
31
 
12
32
  2.0.1 — 2026-04-15
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.1)
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)
@@ -54,7 +53,6 @@ GEM
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
58
  ipaddr (1.2.8)
@@ -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.1'
6
+ VERSION = '2.0.2'
7
7
  end
data/otto.gemspec CHANGED
@@ -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.1
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