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
@@ -1,361 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "fiddle"
4
-
5
- module SafeImage
6
- # Minimal Fiddle binding for libvips. Instead of libvips' variadic C
7
- # convenience API, operations are invoked through the fixed-signature
8
- # GObject layer: vips_operation_new -> set properties as GValues ->
9
- # vips_cache_operation_build -> read outputs -> unref. The invocation
10
- # pattern is modelled on ruby-vips (MIT, Copyright (c) 2016 John Cupitt,
11
- # https://github.com/libvips/ruby-vips), trimmed to exactly the operations
12
- # SafeImage::Native performs — the function table below doubles as an
13
- # operation allowlist.
14
- module VipsGlue
15
- GVALUE_SIZE = 24
16
- GVALUE_ZERO = ("\0" * GVALUE_SIZE).freeze
17
- # Public, decades-stable GParamSpec ABI: GTypeInstance(8) + name(8) +
18
- # flags(4 + padding) puts value_type at byte 24.
19
- PSPEC_VALUE_TYPE_OFFSET = 24
20
-
21
- LIBRARY_CANDIDATES = %w[libvips.so.42 libvips.42.dylib libvips.dylib libvips.so].freeze
22
-
23
- TYPE = {
24
- void: Fiddle::TYPE_VOID,
25
- int: Fiddle::TYPE_INT,
26
- double: Fiddle::TYPE_DOUBLE,
27
- size_t: Fiddle::TYPE_SIZE_T,
28
- ptr: Fiddle::TYPE_VOIDP
29
- }.freeze
30
-
31
- # Fixed-signature entry points only; no varargs anywhere. The g_*
32
- # symbols resolve through the libvips handle via its GLib dependency.
33
- SIGNATURES = {
34
- vips_init: [%i[ptr], :int],
35
- vips_version: [%i[int], :int],
36
- vips_error_buffer: [[], :ptr],
37
- vips_error_clear: [[], :void],
38
- vips_block_untrusted_set: [%i[int], :void],
39
- vips_operation_block_set: [%i[ptr int], :void],
40
- vips_concurrency_set: [%i[int], :void],
41
- vips_cache_set_max: [%i[int], :void],
42
- vips_cache_set_max_mem: [%i[size_t], :void],
43
- vips_cache_set_max_files: [%i[int], :void],
44
- vips_type_find: [%i[ptr ptr], :size_t],
45
- vips_enum_from_nick: [%i[ptr size_t ptr], :int],
46
- vips_operation_new: [%i[ptr], :ptr],
47
- vips_cache_operation_build: [%i[ptr], :ptr],
48
- vips_object_unref_outputs: [%i[ptr], :void],
49
- vips_value_set_array_double: [%i[ptr ptr int], :void],
50
- vips_image_get_width: [%i[ptr], :int],
51
- vips_image_get_height: [%i[ptr], :int],
52
- vips_image_get_bands: [%i[ptr], :int],
53
- vips_image_get_n_pages: [%i[ptr], :int],
54
- vips_image_get_orientation: [%i[ptr], :int],
55
- vips_image_hasalpha: [%i[ptr], :int],
56
- vips_colourspace_issupported: [%i[ptr], :int],
57
- vips_image_new_from_memory_copy: [%i[ptr size_t int int int int], :ptr],
58
- vips_image_write_to_memory: [%i[ptr ptr], :ptr],
59
- g_object_ref: [%i[ptr], :ptr],
60
- g_object_unref: [%i[ptr], :void],
61
- g_object_set_property: [%i[ptr ptr ptr], :void],
62
- g_object_get_property: [%i[ptr ptr ptr], :void],
63
- g_object_class_find_property: [%i[ptr ptr], :ptr],
64
- g_value_init: [%i[ptr size_t], :ptr],
65
- g_value_unset: [%i[ptr], :void],
66
- g_value_set_boolean: [%i[ptr int], :void],
67
- g_value_set_int: [%i[ptr int], :void],
68
- g_value_set_double: [%i[ptr double], :void],
69
- g_value_set_string: [%i[ptr ptr], :void],
70
- g_value_set_enum: [%i[ptr int], :void],
71
- g_value_set_flags: [%i[ptr int], :void],
72
- g_value_set_object: [%i[ptr ptr], :void],
73
- g_value_get_object: [%i[ptr], :ptr],
74
- g_type_fundamental: [%i[size_t], :size_t],
75
- g_type_from_name: [%i[ptr], :size_t],
76
- g_free: [%i[ptr], :void]
77
- }.freeze
78
-
79
- @initialized = false
80
- @load_error = nil
81
- @init_mutex = Mutex.new
82
-
83
- class << self
84
- def init!
85
- return if @initialized
86
- raise @load_error if @load_error
87
-
88
- @init_mutex.synchronize do
89
- next if @initialized
90
- raise @load_error if @load_error
91
-
92
- handle = open_library
93
- @functions = SIGNATURES.to_h do |name, (args, ret)|
94
- address = handle[name.to_s]
95
- [name, Fiddle::Function.new(address, args.map { |t| TYPE.fetch(t) }, TYPE.fetch(ret))]
96
- end
97
-
98
- silence_vips_log!
99
- raise Error, "vips_init failed: #{error_message}" if c(:vips_init, "safe_image") != 0
100
-
101
- major = c(:vips_version, 0)
102
- minor = c(:vips_version, 1)
103
- @version = [major, minor]
104
- raise Error, "libvips >= 8.13 is required (found #{major}.#{minor})" if (@version <=> [8, 13]).negative?
105
-
106
- harden!
107
- resolve_gtypes!
108
- @initialized = true
109
- end
110
- end
111
-
112
- def version
113
- init!
114
- @version
115
- end
116
-
117
- # True when libvips loaded (or loads) successfully. A load failure is
118
- # memoized; the gem keeps working through the ImageMagick paths.
119
- def available?
120
- init!
121
- true
122
- rescue VipsUnavailableError
123
- false
124
- end
125
-
126
- # Calls a bound C function by name.
127
- def c(name, *args)
128
- @functions.fetch(name).call(*args)
129
- end
130
-
131
- def error!
132
- message = error_message
133
- c(:vips_error_clear)
134
- raise InvalidImageError, message
135
- end
136
-
137
- def error_message
138
- ptr = c(:vips_error_buffer)
139
- message = ptr.null? ? "" : ptr.to_s
140
- message.empty? ? "libvips error" : message.strip
141
- end
142
-
143
- def type_find?(nickname)
144
- init!
145
- !c(:vips_type_find, "VipsOperation", nickname).zero?
146
- end
147
-
148
- def unref(image_ptr)
149
- c(:g_object_unref, image_ptr) if image_ptr && !image_ptr.null?
150
- end
151
-
152
- # Tracks every acquired VipsImage pointer and releases all of them when
153
- # the block exits, success or failure. Pipelines are strictly linear,
154
- # so deterministic unref in reverse order is sufficient.
155
- def with_images
156
- acquired = []
157
- track = lambda do |ptr|
158
- acquired << ptr
159
- ptr
160
- end
161
- init!
162
- yield track
163
- ensure
164
- acquired.reverse_each { |ptr| unref(ptr) }
165
- end
166
-
167
- # Invokes one vips operation. Property values are converted according
168
- # to the property's GType: booleans, ints, doubles, strings, enums
169
- # (given as nick strings), flags, double arrays and VipsImage pointers.
170
- # Returns the named output image pointer (caller owns one reference),
171
- # or nil when output is nil (savers).
172
- def operation(nickname, inputs, output: "out")
173
- op = c(:vips_operation_new, nickname)
174
- raise UnsupportedFormatError, "unknown vips operation: #{nickname}" if op.null?
175
-
176
- begin
177
- inputs.each { |name, value| set_property(op, name.to_s, value) }
178
-
179
- built = c(:vips_cache_operation_build, op)
180
- if built.null?
181
- c(:vips_object_unref_outputs, op)
182
- error!
183
- end
184
-
185
- begin
186
- output ? image_output(built, output) : nil
187
- ensure
188
- c(:vips_object_unref_outputs, built)
189
- c(:g_object_unref, built)
190
- end
191
- ensure
192
- c(:g_object_unref, op)
193
- end
194
- end
195
-
196
- def width(image_ptr) = c(:vips_image_get_width, image_ptr)
197
- def height(image_ptr) = c(:vips_image_get_height, image_ptr)
198
- def bands(image_ptr) = c(:vips_image_get_bands, image_ptr)
199
- def pages(image_ptr) = c(:vips_image_get_n_pages, image_ptr)
200
- def orientation(image_ptr) = c(:vips_image_get_orientation, image_ptr)
201
- def alpha?(image_ptr) = !c(:vips_image_hasalpha, image_ptr).zero?
202
- def colourspace_supported?(image_ptr) = !c(:vips_colourspace_issupported, image_ptr).zero?
203
-
204
- def image_from_memory(bytes, width, height, bands, format_number)
205
- init!
206
- ptr = c(:vips_image_new_from_memory_copy, bytes, bytes.bytesize, width, height, bands, format_number)
207
- error! if ptr.null?
208
- ptr
209
- end
210
-
211
- # Copies the image's pixel data out as a binary string (used to read
212
- # the tiny vips_stats matrix without binding the variadic getpoint).
213
- def image_bytes(image_ptr)
214
- size_out = Fiddle::Pointer.malloc(Fiddle::SIZEOF_SIZE_T, Fiddle::RUBY_FREE)
215
- buffer = c(:vips_image_write_to_memory, image_ptr, size_out)
216
- error! if buffer.null?
217
- begin
218
- buffer[0, size_out[0, Fiddle::SIZEOF_SIZE_T].unpack1("J")]
219
- ensure
220
- c(:g_free, buffer)
221
- end
222
- end
223
-
224
- private
225
-
226
- def open_library
227
- errors = []
228
- override = ENV["SAFE_IMAGE_LIBVIPS"]
229
- # An explicit override is authoritative: no fallback to the default
230
- # names (this also lets tests simulate a vips-less host).
231
- candidates = override && !override.empty? ? [override] : LIBRARY_CANDIDATES
232
- candidates.each do |name|
233
- return Fiddle.dlopen(name)
234
- rescue Fiddle::DLError => e
235
- errors << e.message
236
- end
237
- @load_error = VipsUnavailableError.new(
238
- "could not load libvips (install the libvips runtime package, e.g. libvips42 on Debian): #{errors.join("; ")}"
239
- )
240
- raise @load_error
241
- end
242
-
243
- def harden!
244
- # Block operations libvips tags unsafe for untrusted input, plus the
245
- # ImageMagick loader classes by name. The libjxl loader/saver are
246
- # deliberately re-enabled: JPEG XL is part of the supported input
247
- # surface and inputs still pass extension routing and pixel caps.
248
- c(:vips_block_untrusted_set, 1)
249
- c(:vips_operation_block_set, "VipsForeignLoadMagick", 1)
250
- c(:vips_operation_block_set, "VipsForeignLoadMagick6", 1)
251
- c(:vips_operation_block_set, "VipsForeignLoadMagick7", 1)
252
- c(:vips_operation_block_set, "VipsForeignLoadJxl", 0)
253
- c(:vips_operation_block_set, "VipsForeignSaveJxl", 0)
254
-
255
- # Keep the embedded path predictable and bounded.
256
- c(:vips_concurrency_set, 1)
257
- c(:vips_cache_set_max, 0)
258
- c(:vips_cache_set_max_mem, 0)
259
- c(:vips_cache_set_max_files, 0)
260
- end
261
-
262
- # Hostile input is expected here; libvips' GLib warnings about it (for
263
- # example "Not a PNG file") would otherwise litter stderr on every
264
- # rejected upload. Failures still surface as exceptions with the same
265
- # detail. Setting VIPS_WARNING makes vips_init install its own C-level
266
- # no-op log handler — this must NOT be done with a Ruby callback, which
267
- # libvips may invoke from non-Ruby threads and crash the VM. Set
268
- # SAFE_IMAGE_VIPS_WARNINGS=1 to keep the warnings.
269
- def silence_vips_log!
270
- if ENV["SAFE_IMAGE_VIPS_WARNINGS"] == "1"
271
- # VIPS_WARNING may be inherited from a parent process where this
272
- # gem set it; the explicit opt-in to warnings wins.
273
- ENV.delete("VIPS_WARNING")
274
- else
275
- ENV["VIPS_WARNING"] ||= "1"
276
- end
277
- end
278
-
279
- def resolve_gtypes!
280
- @gtype = {}
281
- {
282
- boolean: "gboolean",
283
- int: "gint",
284
- uint64: "guint64",
285
- double: "gdouble",
286
- string: "gchararray",
287
- enum: "GEnum",
288
- flags: "GFlags",
289
- boxed: "GBoxed",
290
- object: "GObject",
291
- image: "VipsImage",
292
- array_double: "VipsArrayDouble"
293
- }.each do |key, name|
294
- gtype = c(:g_type_from_name, name)
295
- raise Error, "GType #{name} is not registered" if gtype.zero?
296
- @gtype[key] = gtype
297
- end
298
- end
299
-
300
- def with_gvalue(gtype)
301
- buffer = Fiddle::Pointer.malloc(GVALUE_SIZE, Fiddle::RUBY_FREE)
302
- buffer[0, GVALUE_SIZE] = GVALUE_ZERO
303
- c(:g_value_init, buffer, gtype)
304
- begin
305
- yield buffer
306
- ensure
307
- c(:g_value_unset, buffer)
308
- end
309
- end
310
-
311
- def set_property(object_ptr, name, value)
312
- name = name.tr("_", "-")
313
- klass = object_ptr[0, Fiddle::SIZEOF_VOIDP].unpack1("J")
314
- pspec = c(:g_object_class_find_property, klass, name)
315
- raise Error, "vips operation has no argument #{name.inspect}" if pspec.null?
316
-
317
- value_type = pspec[PSPEC_VALUE_TYPE_OFFSET, Fiddle::SIZEOF_VOIDP].unpack1("J")
318
- with_gvalue(value_type) do |gvalue|
319
- write_gvalue(gvalue, value_type, name, value)
320
- c(:g_object_set_property, object_ptr, name, gvalue)
321
- end
322
- end
323
-
324
- def write_gvalue(gvalue, value_type, name, value)
325
- case c(:g_type_fundamental, value_type)
326
- when @gtype[:boolean] then c(:g_value_set_boolean, gvalue, value ? 1 : 0)
327
- when @gtype[:int] then c(:g_value_set_int, gvalue, Integer(value))
328
- when @gtype[:double] then c(:g_value_set_double, gvalue, Float(value))
329
- when @gtype[:string] then c(:g_value_set_string, gvalue, value.to_s)
330
- when @gtype[:enum] then c(:g_value_set_enum, gvalue, enum_value(value_type, value))
331
- when @gtype[:flags] then c(:g_value_set_flags, gvalue, Integer(value))
332
- when @gtype[:object] then c(:g_value_set_object, gvalue, value)
333
- when @gtype[:boxed]
334
- raise Error, "unsupported boxed type for #{name.inspect}" unless value_type == @gtype[:array_double]
335
- doubles = Array(value).map { |v| Float(v) }
336
- c(:vips_value_set_array_double, gvalue, doubles.pack("d*"), doubles.length)
337
- else
338
- raise Error, "unsupported GType for vips argument #{name.inspect}"
339
- end
340
- end
341
-
342
- def enum_value(value_type, value)
343
- return Integer(value) if value.is_a?(Integer)
344
-
345
- number = c(:vips_enum_from_nick, "safe_image", value_type, value.to_s)
346
- error! if number.negative?
347
- number
348
- end
349
-
350
- def image_output(op_ptr, name)
351
- with_gvalue(@gtype[:image]) do |gvalue|
352
- c(:g_object_get_property, op_ptr, name, gvalue)
353
- ptr = c(:g_value_get_object, gvalue)
354
- raise InvalidImageError, "vips operation produced no output" if ptr.null?
355
- c(:g_object_ref, ptr)
356
- ptr
357
- end
358
- end
359
- end
360
- end
361
- end