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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +61 -12
- data/README.md +140 -287
- data/SECURITY.md +22 -11
- data/docs/architecture.md +63 -0
- data/ext/safe_image_vips_helper/extconf.rb +43 -0
- data/ext/safe_image_vips_helper/safe_image_vips_helper.c +1007 -0
- data/lib/safe_image/api/metadata.rb +85 -0
- data/lib/safe_image/api/transform.rb +152 -0
- data/lib/safe_image/backend_label.rb +24 -0
- data/lib/safe_image/formats.rb +96 -0
- data/lib/safe_image/ico.rb +42 -40
- data/lib/safe_image/image_magick_backend.rb +219 -162
- data/lib/safe_image/jpegli_backend.rb +64 -44
- data/lib/safe_image/metadata_operations.rb +155 -0
- data/lib/safe_image/native.rb +96 -290
- data/lib/safe_image/native_helper.rb +281 -0
- data/lib/safe_image/operation_backends/base.rb +83 -0
- data/lib/safe_image/operation_backends/image_magick.rb +123 -0
- data/lib/safe_image/operation_backends/vips.rb +251 -0
- data/lib/safe_image/operation_backends.rb +27 -0
- data/lib/safe_image/operation_set.rb +22 -0
- data/lib/safe_image/optimizer.rb +225 -98
- data/lib/safe_image/path_safety.rb +11 -0
- data/lib/safe_image/processor.rb +122 -85
- data/lib/safe_image/quality_defaults.rb +23 -0
- data/lib/safe_image/remote.rb +248 -144
- data/lib/safe_image/result.rb +71 -23
- data/lib/safe_image/runner.rb +60 -23
- data/lib/safe_image/sandbox.rb +44 -218
- data/lib/safe_image/staged_output.rb +44 -0
- data/lib/safe_image/svg_metadata.rb +74 -69
- data/lib/safe_image/transform_operations.rb +139 -0
- data/lib/safe_image/version.rb +1 -1
- data/lib/safe_image/vips_backend.rb +13 -12
- data/lib/safe_image.rb +62 -306
- metadata +43 -37
- data/lib/safe_image/discourse_compat.rb +0 -441
- data/lib/safe_image/svg_css.rb +0 -314
- data/lib/safe_image/svg_sanitizer.rb +0 -583
- data/lib/safe_image/vips_glue.rb +0 -361
- data/lib/safe_image/zygote.rb +0 -619
data/lib/safe_image/vips_glue.rb
DELETED
|
@@ -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
|