safe_image 0.3.0 → 0.5.0

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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +61 -12
  3. data/README.md +140 -287
  4. data/SECURITY.md +22 -11
  5. data/docs/architecture.md +63 -0
  6. data/ext/safe_image_vips_helper/extconf.rb +43 -0
  7. data/ext/safe_image_vips_helper/safe_image_vips_helper.c +1007 -0
  8. data/lib/safe_image/api/metadata.rb +85 -0
  9. data/lib/safe_image/api/transform.rb +152 -0
  10. data/lib/safe_image/backend_label.rb +24 -0
  11. data/lib/safe_image/formats.rb +96 -0
  12. data/lib/safe_image/ico.rb +42 -40
  13. data/lib/safe_image/image_magick_backend.rb +219 -162
  14. data/lib/safe_image/jpegli_backend.rb +64 -44
  15. data/lib/safe_image/metadata_operations.rb +155 -0
  16. data/lib/safe_image/native.rb +96 -290
  17. data/lib/safe_image/native_helper.rb +281 -0
  18. data/lib/safe_image/operation_backends/base.rb +83 -0
  19. data/lib/safe_image/operation_backends/image_magick.rb +123 -0
  20. data/lib/safe_image/operation_backends/vips.rb +251 -0
  21. data/lib/safe_image/operation_backends.rb +27 -0
  22. data/lib/safe_image/operation_set.rb +22 -0
  23. data/lib/safe_image/optimizer.rb +225 -98
  24. data/lib/safe_image/path_safety.rb +11 -0
  25. data/lib/safe_image/processor.rb +122 -85
  26. data/lib/safe_image/quality_defaults.rb +23 -0
  27. data/lib/safe_image/remote.rb +248 -144
  28. data/lib/safe_image/result.rb +71 -23
  29. data/lib/safe_image/runner.rb +60 -23
  30. data/lib/safe_image/sandbox.rb +44 -218
  31. data/lib/safe_image/staged_output.rb +44 -0
  32. data/lib/safe_image/svg_metadata.rb +74 -69
  33. data/lib/safe_image/transform_operations.rb +139 -0
  34. data/lib/safe_image/version.rb +1 -1
  35. data/lib/safe_image/vips_backend.rb +13 -12
  36. data/lib/safe_image.rb +62 -306
  37. metadata +43 -37
  38. data/lib/safe_image/discourse_compat.rb +0 -441
  39. data/lib/safe_image/svg_css.rb +0 -314
  40. data/lib/safe_image/svg_sanitizer.rb +0 -583
  41. data/lib/safe_image/vips_glue.rb +0 -361
  42. data/lib/safe_image/zygote.rb +0 -619
data/lib/safe_image.rb CHANGED
@@ -3,22 +3,40 @@
3
3
  require_relative "safe_image/version"
4
4
 
5
5
  module SafeImage
6
- class Error < StandardError; end
6
+ class Error < StandardError
7
+ attr_reader :original_error_class, :stderr_tail
8
+
9
+ def initialize(message = nil, original_error_class: nil, stderr_tail: nil)
10
+ super(message)
11
+ @original_error_class = original_error_class
12
+ @stderr_tail = stderr_tail
13
+ end
14
+ end
7
15
 
8
16
  # Raised when any operation is attempted before SafeImage.configure!.
9
- class NotConfiguredError < Error; end
17
+ class NotConfiguredError < Error
18
+ end
19
+
20
+ class UnsupportedFormatError < Error
21
+ end
10
22
 
11
- class UnsupportedFormatError < Error; end
23
+ # Raised when the native libvips helper cannot initialize or execute.
24
+ # configure!(backend: :vips) surfaces this at boot; operations never fall back
25
+ # to ImageMagick.
26
+ class VipsUnavailableError < UnsupportedFormatError
27
+ end
28
+
29
+ class UnsafePathError < Error
30
+ end
12
31
 
13
- # Raised when libvips cannot be loaded at runtime. configure!(backend: :vips)
14
- # surfaces this at boot; operations never fall back to ImageMagick.
15
- class VipsUnavailableError < UnsupportedFormatError; end
16
- class UnsafePathError < Error; end
17
- class InvalidImageError < Error; end
18
- class LimitError < Error; end
32
+ class InvalidImageError < Error
33
+ end
34
+
35
+ class LimitError < Error
36
+ end
19
37
 
20
38
  # Default decompression-bomb ceiling when configure! is not given an explicit
21
- # max_pixels. Mirrored in the native binding (SAFE_IMAGE_DEFAULT_MAX_PIXELS)
39
+ # max_pixels. Mirrored in the native helper (SAFE_IMAGE_DEFAULT_MAX_PIXELS)
22
40
  # and aligned with the 128MP area limit on the ImageMagick path. Per-call
23
41
  # max_pixels: overrides the configured value.
24
42
  DEFAULT_MAX_PIXELS = 128 * 1024 * 1024
@@ -33,40 +51,49 @@ end
33
51
 
34
52
  require_relative "safe_image/native"
35
53
  require_relative "safe_image/result"
54
+ require_relative "safe_image/quality_defaults"
36
55
  require_relative "safe_image/runner"
37
- require_relative "safe_image/sandbox"
38
- require_relative "safe_image/zygote"
39
56
  require_relative "safe_image/path_safety"
57
+ require_relative "safe_image/formats"
58
+ require_relative "safe_image/backend_label"
59
+ require_relative "safe_image/operation_set"
60
+ require_relative "safe_image/staged_output"
61
+ require_relative "safe_image/sandbox"
62
+ require_relative "safe_image/native_helper"
40
63
  require_relative "safe_image/optimizer"
41
64
  require_relative "safe_image/svg_metadata"
42
- require_relative "safe_image/svg_css"
43
- require_relative "safe_image/svg_sanitizer"
44
65
  require_relative "safe_image/remote"
45
66
  require_relative "safe_image/ico"
46
67
  require_relative "safe_image/image_magick_backend"
47
68
  require_relative "safe_image/jpegli_backend"
48
69
  require_relative "safe_image/vips_backend"
49
70
  require_relative "safe_image/processor"
50
- require_relative "safe_image/discourse_compat"
71
+ require_relative "safe_image/operation_backends"
72
+ require_relative "safe_image/metadata_operations"
73
+ require_relative "safe_image/transform_operations"
74
+ require_relative "safe_image/api/metadata"
75
+ require_relative "safe_image/api/transform"
51
76
 
52
77
  module SafeImage
78
+ private_constant :API
79
+
53
80
  module_function
54
81
 
55
82
  @config = nil
56
83
 
57
84
  # Decides, in one place, everything that varies by host: which backend
58
- # decodes untrusted bytes, whether operations run inside the Landlock
59
- # sandbox, and the default decompression-bomb ceiling. Must be called before
60
- # any operation; calling it again replaces the configuration.
85
+ # decodes untrusted bytes, whether child helpers/tools run under Landlock,
86
+ # and the default decompression-bomb ceiling. Must be called before any
87
+ # operation; calling it again replaces the configuration.
61
88
  #
62
89
  # Validation is eager so a misconfigured host fails at boot rather than on
63
90
  # the first request.
64
91
  def configure!(backend:, landlock:, max_pixels: DEFAULT_MAX_PIXELS)
65
92
  backend = backend.to_sym
66
- unless BACKENDS.include?(backend)
93
+ if BACKENDS.none? { |candidate| candidate == backend }
67
94
  raise ArgumentError, "unknown backend: #{backend.inspect} (expected :vips or :imagemagick)"
68
95
  end
69
- unless [true, false].include?(landlock)
96
+ unless [true, false].any? { |candidate| candidate == landlock }
70
97
  raise ArgumentError, "landlock must be true or false, got: #{landlock.inspect}"
71
98
  end
72
99
  max_pixels = Integer(max_pixels)
@@ -75,9 +102,9 @@ module SafeImage
75
102
  case backend
76
103
  when :vips
77
104
  begin
78
- VipsGlue.init!
79
- rescue VipsUnavailableError => e
80
- raise Error, "backend: :vips requested but libvips is unavailable: #{e.message}"
105
+ NativeHelper.verify!
106
+ rescue Error => e
107
+ raise Error, "backend: :vips requested but the native libvips helper is unavailable: #{e.message}"
81
108
  end
82
109
  when :imagemagick
83
110
  unless Runner.available?("magick") || Runner.available?("convert")
@@ -87,305 +114,34 @@ module SafeImage
87
114
  if landlock && !Sandbox.available?
88
115
  raise Error, "landlock: true requested but the Landlock sandbox is unavailable on this host"
89
116
  end
90
-
91
- # The zygote bakes the backend and max_pixels in at boot; a reconfigure
92
- # must not serve from a stale one.
93
- Zygote.shutdown!
117
+ NativeHelper.ensure_available! if landlock && backend == :vips
94
118
 
95
119
  @config = Config.new(backend: backend, landlock: landlock, max_pixels: max_pixels)
96
120
  end
97
121
 
98
122
  def config
99
- @config || raise(NotConfiguredError, "call SafeImage.configure!(backend: :vips | :imagemagick, landlock: true | false) before using SafeImage")
123
+ @config ||
124
+ raise(
125
+ NotConfiguredError,
126
+ "call SafeImage.configure!(backend: :vips | :imagemagick, landlock: true | false) before using SafeImage"
127
+ )
100
128
  end
101
129
 
102
130
  def configured? = !@config.nil?
103
131
 
104
132
  def sandbox_available? = Sandbox.available?
105
133
 
106
- # Internal: whether operations must route through the sandbox worker. False
107
- # before configure! (so configure!'s own availability probes can run
108
- # commands) and inside worker children (so sandboxed operations never nest).
134
+ # Internal: whether child commands/helpers must run under Landlock. False
135
+ # before configure! so configure!'s own availability probes can run.
109
136
  def sandbox?
110
- !!@config&.landlock && ENV["SAFE_IMAGE_SANDBOX_CHILD"] != "1"
137
+ !!@config&.landlock
111
138
  end
112
139
 
113
140
  # Internal: per-call max_pixels overrides the configured default.
114
- def resolved_max_pixels(max_pixels)
141
+ def resolved_max_pixels(max_pixels, config: self.config)
115
142
  max_pixels.nil? ? config.max_pixels : max_pixels
116
143
  end
117
144
 
118
- def maybe_sandbox(operation, args: [], kwargs: {})
119
- config
120
- return yield unless sandbox?
121
-
122
- Sandbox.public_call!(operation, args: args, kwargs: kwargs)
123
- end
124
-
125
- def probe(path, max_pixels: nil)
126
- maybe_sandbox(:probe, args: [path], kwargs: { max_pixels: max_pixels }) do
127
- path = PathSafety.local_path(path)
128
- max_pixels = resolved_max_pixels(max_pixels)
129
-
130
- case File.extname(path).downcase
131
- when ".svg"
132
- info = SvgMetadata.probe(path, max_pixels: max_pixels)
133
- Result.new(
134
- input: File.expand_path(path),
135
- output: nil,
136
- input_format: "svg",
137
- output_format: nil,
138
- width: info.fetch(:width),
139
- height: info.fetch(:height),
140
- filesize: File.size(path),
141
- backend: "svg-metadata",
142
- duration_ms: info.fetch(:duration_ms),
143
- optimizer: nil
144
- )
145
- when ".ico"
146
- # Pure-Ruby directory parse; reports the largest entry's dimensions.
147
- info = Ico.probe(path, max_pixels: max_pixels)
148
- Result.new(
149
- input: File.expand_path(path),
150
- output: nil,
151
- input_format: "ico",
152
- output_format: nil,
153
- width: info.fetch(:width),
154
- height: info.fetch(:height),
155
- filesize: File.size(path),
156
- backend: "ico-metadata",
157
- duration_ms: info.fetch(:duration_ms),
158
- optimizer: nil
159
- )
160
- else
161
- case config.backend
162
- when :vips
163
- Processor.new(max_pixels: max_pixels).probe(path)
164
- when :imagemagick
165
- info = ImageMagickBackend.probe(path, max_pixels: max_pixels)
166
- Result.new(
167
- input: File.expand_path(path),
168
- output: nil,
169
- input_format: info.fetch(:input_format),
170
- output_format: nil,
171
- width: info.fetch(:width),
172
- height: info.fetch(:height),
173
- filesize: File.size(path),
174
- backend: "imagemagick",
175
- duration_ms: info.fetch(:duration_ms),
176
- optimizer: nil
177
- )
178
- end
179
- end
180
- end
181
- end
182
-
183
- def type(path, max_pixels: nil)
184
- maybe_sandbox(:type, args: [path], kwargs: { max_pixels: max_pixels }) do
185
- fastimage_type(probe(path, max_pixels: max_pixels).input_format)
186
- end
187
- end
188
-
189
- def size(path, max_pixels: nil)
190
- maybe_sandbox(:size, args: [path], kwargs: { max_pixels: max_pixels }) do
191
- result = probe(path, max_pixels: max_pixels)
192
- [result.width, result.height]
193
- end
194
- end
195
-
196
- def dimensions(path, max_pixels: nil)
197
- size(path, max_pixels: max_pixels)
198
- end
199
-
200
- def info(path, max_pixels: nil, animated: false, orientation: false)
201
- maybe_sandbox(:info, args: [path], kwargs: { max_pixels: max_pixels, animated: animated, orientation: orientation }) do
202
- result = probe(path, max_pixels: max_pixels)
203
- type = fastimage_type(result.input_format)
204
- Info.new(
205
- path: result.input,
206
- type: type,
207
- width: result.width,
208
- height: result.height,
209
- size: [result.width, result.height],
210
- animated: animated ? animated?(path, max_pixels: max_pixels) : nil,
211
- orientation: orientation ? orientation(path, max_pixels: max_pixels) : nil
212
- )
213
- end
214
- end
215
-
216
- def orientation(path, max_pixels: nil)
217
- maybe_sandbox(:orientation, args: [path], kwargs: { max_pixels: max_pixels }) do
218
- case File.extname(PathSafety.local_path(path)).downcase
219
- when ".svg", ".ico"
220
- # No EXIF orientation in either format; upright by definition.
221
- 1
222
- else
223
- max_pixels = resolved_max_pixels(max_pixels)
224
- case config.backend
225
- when :vips
226
- # Header-only native read.
227
- VipsBackend.orientation(path, max_pixels: max_pixels)
228
- when :imagemagick
229
- # Probe first: rejects undecodable files and enforces the pixel cap.
230
- ImageMagickBackend.probe(path, max_pixels: max_pixels)
231
- ImageMagickBackend.orientation(path)
232
- end
233
- end
234
- end
235
- end
236
-
237
- def dominant_color(path, max_pixels: nil)
238
- maybe_sandbox(:dominant_color, args: [path], kwargs: { max_pixels: max_pixels }) do
239
- max_pixels = resolved_max_pixels(max_pixels)
240
- case config.backend
241
- when :vips
242
- if File.extname(PathSafety.local_path(path)).downcase == ".ico"
243
- # Pure-Ruby ICO decode; vips only averages the decoded pixels.
244
- Ico.dominant_color(path, max_pixels: max_pixels)
245
- else
246
- VipsBackend.dominant_color(path, max_pixels: max_pixels)
247
- end
248
- when :imagemagick
249
- imagemagick_dominant_color(path, max_pixels: max_pixels)
250
- end
251
- end
252
- end
253
-
254
- def imagemagick_dominant_color(path, max_pixels:)
255
- # Probe first: rejects undecodable files and enforces the pixel cap
256
- # before ImageMagick fully decodes the image to average it.
257
- probe(path, max_pixels: max_pixels)
258
- ImageMagickBackend.dominant_color(path)
259
- end
260
-
261
- def fastimage_type(format)
262
- format.to_s == "jpg" ? :jpeg : format.to_s.to_sym
263
- end
264
-
265
- def remote_info(url, **kwargs)
266
- config
267
- Remote.info(url, **kwargs)
268
- end
269
-
270
- def remote_size(url, **kwargs)
271
- config
272
- Remote.size(url, **kwargs)
273
- end
274
-
275
- def remote_dimensions(url, **kwargs)
276
- remote_size(url, **kwargs)
277
- end
278
-
279
- def remote_type(url, **kwargs)
280
- config
281
- Remote.type(url, **kwargs)
282
- end
283
-
284
- def remote_animated?(url, **kwargs)
285
- config
286
- Remote.animated?(url, **kwargs)
287
- end
288
-
289
- def remote_dominant_color(url, **kwargs)
290
- config
291
- Remote.dominant_color(url, **kwargs)
292
- end
293
-
294
- def fetch_remote(url, **kwargs, &block)
295
- config
296
- Remote.fetch(url, **kwargs, &block)
297
- end
298
-
299
- def thumbnail(input:, output:, width:, height:, format: nil, quality: 85, max_pixels: nil, optimize: false, optimize_mode: :lossless, chroma_subsampling: :auto)
300
- maybe_sandbox(
301
- :thumbnail,
302
- kwargs: {
303
- input: input,
304
- output: output,
305
- width: width,
306
- height: height,
307
- format: format,
308
- quality: quality,
309
- max_pixels: max_pixels,
310
- optimize: optimize,
311
- optimize_mode: optimize_mode,
312
- chroma_subsampling: chroma_subsampling
313
- }
314
- ) do
315
- Processor.new(max_pixels: resolved_max_pixels(max_pixels), chroma_subsampling: chroma_subsampling).thumbnail(
316
- input: input,
317
- output: output,
318
- width: width,
319
- height: height,
320
- format: format,
321
- quality: quality,
322
- optimize: optimize,
323
- optimize_mode: optimize_mode
324
- )
325
- end
326
- end
327
-
328
- def optimize(path, mode: :lossless, strip_metadata: true, quality: nil, strict: true)
329
- maybe_sandbox(:optimize, args: [path], kwargs: { mode: mode, strip_metadata: strip_metadata, quality: quality, strict: strict }) do
330
- Optimizer.optimize(path, mode: mode, strip_metadata: strip_metadata, quality: quality, strict: strict)
331
- end
332
- end
333
-
334
- def resize(*args, **kwargs)
335
- maybe_sandbox(:resize, args: args, kwargs: kwargs) { DiscourseCompat.resize(*args, **kwargs) }
336
- end
337
-
338
- def crop(*args, **kwargs)
339
- maybe_sandbox(:crop, args: args, kwargs: kwargs) { DiscourseCompat.crop(*args, **kwargs) }
340
- end
341
-
342
- def downsize(*args, **kwargs)
343
- maybe_sandbox(:downsize, args: args, kwargs: kwargs) { DiscourseCompat.downsize(*args, **kwargs) }
344
- end
345
-
346
- def convert(*args, **kwargs)
347
- maybe_sandbox(:convert, args: args, kwargs: kwargs) { DiscourseCompat.convert(*args, **kwargs) }
348
- end
349
-
350
- def convert_to_jpeg(*args, **kwargs)
351
- maybe_sandbox(:convert_to_jpeg, args: args, kwargs: kwargs) { DiscourseCompat.convert_to_jpeg(*args, **kwargs) }
352
- end
353
-
354
- def fix_orientation(*args, **kwargs)
355
- maybe_sandbox(:fix_orientation, args: args, kwargs: kwargs) { DiscourseCompat.fix_orientation(*args, **kwargs) }
356
- end
357
-
358
- def convert_favicon_to_png(*args, **kwargs)
359
- maybe_sandbox(:convert_favicon_to_png, args: args, kwargs: kwargs) { DiscourseCompat.convert_favicon_to_png(*args, **kwargs) }
360
- end
361
-
362
- def frame_count(*args, **kwargs)
363
- maybe_sandbox(:frame_count, args: args, kwargs: kwargs) { DiscourseCompat.frame_count(*args, **kwargs) }
364
- end
365
-
366
- def animated?(*args, **kwargs)
367
- config
368
- path = args.first
369
- return false if path && File.extname(PathSafety.local_path(path)).downcase == ".svg"
370
-
371
- maybe_sandbox(:animated?, args: args, kwargs: kwargs) { DiscourseCompat.animated?(*args, **kwargs) }
372
- end
373
-
374
- def letter_avatar(*args, **kwargs)
375
- maybe_sandbox(:letter_avatar, args: args, kwargs: kwargs) { DiscourseCompat.letter_avatar(*args, **kwargs) }
376
- end
377
-
378
- def optimize_image!(*args, **kwargs)
379
- maybe_sandbox(:optimize_image!, args: args, kwargs: kwargs) { DiscourseCompat.optimize_image!(*args, **kwargs) }
380
- end
381
-
382
- def sanitize_svg!(*args, **kwargs)
383
- # Validate the required id_namespace in the parent (after the configured
384
- # check) so omitting/malformed values raise ArgumentError consistently —
385
- # otherwise, under the sandbox, the worker raises and it surfaces as a
386
- # sandbox CommandError instead of the documented ArgumentError.
387
- config
388
- SvgSanitizer.resolve_namespace(kwargs.fetch(:id_namespace, SvgSanitizer::NAMESPACE_REQUIRED))
389
- maybe_sandbox(:sanitize_svg!, args: args, kwargs: kwargs) { SvgSanitizer.sanitize!(*args, **kwargs) }
390
- end
145
+ extend API::Metadata
146
+ extend API::Transform
391
147
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: safe_image
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Saffron
@@ -10,20 +10,6 @@ bindir: bin
10
10
  cert_chain: []
11
11
  date: 1980-01-02 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: fiddle
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '1.0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '1.0'
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: nokogiri
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -39,112 +25,132 @@ dependencies:
39
25
  - !ruby/object:Gem::Version
40
26
  version: '1.16'
41
27
  - !ruby/object:Gem::Dependency
42
- name: rexml
28
+ name: minitest
43
29
  requirement: !ruby/object:Gem::Requirement
44
30
  requirements:
45
31
  - - "~>"
46
32
  - !ruby/object:Gem::Version
47
- version: '3.4'
33
+ version: '5.25'
48
34
  type: :development
49
35
  prerelease: false
50
36
  version_requirements: !ruby/object:Gem::Requirement
51
37
  requirements:
52
38
  - - "~>"
53
39
  - !ruby/object:Gem::Version
54
- version: '3.4'
40
+ version: '5.25'
55
41
  - !ruby/object:Gem::Dependency
56
- name: minitest
42
+ name: rake
57
43
  requirement: !ruby/object:Gem::Requirement
58
44
  requirements:
59
45
  - - "~>"
60
46
  - !ruby/object:Gem::Version
61
- version: '5.25'
47
+ version: '13.0'
62
48
  type: :development
63
49
  prerelease: false
64
50
  version_requirements: !ruby/object:Gem::Requirement
65
51
  requirements:
66
52
  - - "~>"
67
53
  - !ruby/object:Gem::Version
68
- version: '5.25'
54
+ version: '13.0'
69
55
  - !ruby/object:Gem::Dependency
70
- name: rake
56
+ name: rubocop-discourse
71
57
  requirement: !ruby/object:Gem::Requirement
72
58
  requirements:
73
59
  - - "~>"
74
60
  - !ruby/object:Gem::Version
75
- version: '13.0'
61
+ version: '3.18'
76
62
  type: :development
77
63
  prerelease: false
78
64
  version_requirements: !ruby/object:Gem::Requirement
79
65
  requirements:
80
66
  - - "~>"
81
67
  - !ruby/object:Gem::Version
82
- version: '13.0'
68
+ version: '3.18'
83
69
  - !ruby/object:Gem::Dependency
84
- name: rubocop-discourse
70
+ name: syntax_tree
85
71
  requirement: !ruby/object:Gem::Requirement
86
72
  requirements:
87
73
  - - "~>"
88
74
  - !ruby/object:Gem::Version
89
- version: '3.18'
75
+ version: '6.3'
90
76
  type: :development
91
77
  prerelease: false
92
78
  version_requirements: !ruby/object:Gem::Requirement
93
79
  requirements:
94
80
  - - "~>"
95
81
  - !ruby/object:Gem::Version
96
- version: '3.18'
82
+ version: '6.3'
97
83
  - !ruby/object:Gem::Dependency
98
84
  name: landlock
99
85
  requirement: !ruby/object:Gem::Requirement
100
86
  requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.3'
101
90
  - - ">="
102
91
  - !ruby/object:Gem::Version
103
- version: '0'
92
+ version: 0.3.0
104
93
  type: :development
105
94
  prerelease: false
106
95
  version_requirements: !ruby/object:Gem::Requirement
107
96
  requirements:
97
+ - - "~>"
98
+ - !ruby/object:Gem::Version
99
+ version: '0.3'
108
100
  - - ">="
109
101
  - !ruby/object:Gem::Version
110
- version: '0'
102
+ version: 0.3.0
111
103
  description: 'Safe Image is a small Ruby image-processing boundary for untrusted uploads:
112
- direct libvips thumbnails/probing, hardened ImageMagick compatibility operations,
113
- optimisation, SVG sanitising, and optional atomic Landlock sandbox execution.'
104
+ helper-contained libvips thumbnails/probing, hardened ImageMagick compatibility
105
+ operations, optimisation, SVG metadata probing, and optional atomic Landlock sandbox
106
+ execution.'
114
107
  email:
115
108
  - sam@discourse.org
116
109
  executables: []
117
- extensions: []
110
+ extensions:
111
+ - ext/safe_image_vips_helper/extconf.rb
118
112
  extra_rdoc_files: []
119
113
  files:
120
114
  - CHANGELOG.md
121
115
  - LICENSE
122
116
  - README.md
123
117
  - SECURITY.md
118
+ - docs/architecture.md
119
+ - ext/safe_image_vips_helper/extconf.rb
120
+ - ext/safe_image_vips_helper/safe_image_vips_helper.c
124
121
  - lib/safe_image.rb
125
122
  - lib/safe_image/RT_sRGB.icm
126
- - lib/safe_image/discourse_compat.rb
123
+ - lib/safe_image/api/metadata.rb
124
+ - lib/safe_image/api/transform.rb
125
+ - lib/safe_image/backend_label.rb
127
126
  - lib/safe_image/fonts/DEJAVU-LICENSE
128
127
  - lib/safe_image/fonts/DejaVuSans.ttf
128
+ - lib/safe_image/formats.rb
129
129
  - lib/safe_image/ico.rb
130
130
  - lib/safe_image/image_magick_backend.rb
131
131
  - lib/safe_image/imagemagick_policy/policy.xml
132
132
  - lib/safe_image/jpegli_backend.rb
133
+ - lib/safe_image/metadata_operations.rb
133
134
  - lib/safe_image/native.rb
135
+ - lib/safe_image/native_helper.rb
136
+ - lib/safe_image/operation_backends.rb
137
+ - lib/safe_image/operation_backends/base.rb
138
+ - lib/safe_image/operation_backends/image_magick.rb
139
+ - lib/safe_image/operation_backends/vips.rb
140
+ - lib/safe_image/operation_set.rb
134
141
  - lib/safe_image/optimizer.rb
135
142
  - lib/safe_image/path_safety.rb
136
143
  - lib/safe_image/processor.rb
144
+ - lib/safe_image/quality_defaults.rb
137
145
  - lib/safe_image/remote.rb
138
146
  - lib/safe_image/result.rb
139
147
  - lib/safe_image/runner.rb
140
148
  - lib/safe_image/sandbox.rb
141
- - lib/safe_image/svg_css.rb
149
+ - lib/safe_image/staged_output.rb
142
150
  - lib/safe_image/svg_metadata.rb
143
- - lib/safe_image/svg_sanitizer.rb
151
+ - lib/safe_image/transform_operations.rb
144
152
  - lib/safe_image/version.rb
145
153
  - lib/safe_image/vips_backend.rb
146
- - lib/safe_image/vips_glue.rb
147
- - lib/safe_image/zygote.rb
148
154
  homepage: https://github.com/sam-saffron-jarvis/safe-image
149
155
  licenses:
150
156
  - MIT