Skip to content

Commit b4d72bd

Browse files
mceachenlovell
authored andcommitted
Add failOnError option to fail-fast on bad input image data (lovell#976)
1 parent 382d476 commit b4d72bd

10 files changed

Lines changed: 103 additions & 4 deletions

File tree

lib/constructor.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ const debuglog = util.debuglog('sharp');
9393
* a String containing the path to an JPEG, PNG, WebP, GIF, SVG or TIFF image file.
9494
* JPEG, PNG, WebP, GIF, SVG, TIFF or raw pixel image data can be streamed into the object when not present.
9595
* @param {Object} [options] - if present, is an Object with optional attributes.
96+
* @param {Boolean} [options.failOnError=false] - by default apply a "best effort"
97+
* to decode images, even if the data is corrupt or invalid. Set this flag to true
98+
* if you'd rather halt processing and raise an error when loading invalid images.
9699
* @param {Number} [options.density=72] - integral number representing the DPI for vector images.
97100
* @param {Object} [options.raw] - describes raw pixel input image data. See `raw()` for pixel ordering.
98101
* @param {Number} [options.raw.width]

lib/input.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const sharp = require('../build/Release/sharp.node');
99
* @private
1010
*/
1111
function _createInputDescriptor (input, inputOptions, containerOptions) {
12-
const inputDescriptor = {};
12+
const inputDescriptor = { failOnError: false };
1313
if (is.string(input)) {
1414
// filesystem
1515
inputDescriptor.file = input;
@@ -26,6 +26,14 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
2626
throw new Error('Unsupported input ' + typeof input);
2727
}
2828
if (is.object(inputOptions)) {
29+
// Fail on error
30+
if (is.defined(inputOptions.failOnError)) {
31+
if (is.bool(inputOptions.failOnError)) {
32+
inputDescriptor.failOnError = inputOptions.failOnError;
33+
} else {
34+
throw new Error('Invalid failOnError (boolean) ' + inputOptions.failOnError);
35+
}
36+
}
2937
// Density
3038
if (is.defined(inputOptions.density)) {
3139
if (is.integer(inputOptions.density) && is.inRange(inputOptions.density, 1, 2400)) {

src/common.cc

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ namespace sharp {
5252
descriptor->buffer = node::Buffer::Data(buffer);
5353
buffersToPersist.push_back(buffer);
5454
}
55+
descriptor->failOnError = AttrTo<bool>(input, "failOnError");
5556
// Density for vector-based input
5657
if (HasAttr(input, "density")) {
5758
descriptor->density = AttrTo<uint32_t>(input, "density");
@@ -219,7 +220,9 @@ namespace sharp {
219220
imageType = DetermineImageType(descriptor->buffer, descriptor->bufferLength);
220221
if (imageType != ImageType::UNKNOWN) {
221222
try {
222-
vips::VOption *option = VImage::option()->set("access", accessMethod);
223+
vips::VOption *option = VImage::option()
224+
->set("access", accessMethod)
225+
->set("fail", descriptor->failOnError);
223226
if (imageType == ImageType::SVG || imageType == ImageType::PDF) {
224227
option->set("dpi", static_cast<double>(descriptor->density));
225228
}
@@ -256,7 +259,9 @@ namespace sharp {
256259
imageType = DetermineImageType(descriptor->file.data());
257260
if (imageType != ImageType::UNKNOWN) {
258261
try {
259-
vips::VOption *option = VImage::option()->set("access", accessMethod);
262+
vips::VOption *option = VImage::option()
263+
->set("access", accessMethod)
264+
->set("fail", descriptor->failOnError);
260265
if (imageType == ImageType::SVG || imageType == ImageType::PDF) {
261266
option->set("dpi", static_cast<double>(descriptor->density));
262267
}

src/common.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ namespace sharp {
4747
std::string name;
4848
std::string file;
4949
char *buffer;
50+
bool failOnError;
5051
size_t bufferLength;
5152
int density;
5253
int rawChannels;
@@ -59,6 +60,7 @@ namespace sharp {
5960

6061
InputDescriptor():
6162
buffer(nullptr),
63+
failOnError(FALSE),
6264
bufferLength(0),
6365
density(72),
6466
rawChannels(0),

src/pipeline.cc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,9 @@ class PipelineWorker : public Nan::AsyncWorker {
249249
}
250250
if (shrink_on_load > 1) {
251251
// Reload input using shrink-on-load
252-
vips::VOption *option = VImage::option()->set("shrink", shrink_on_load);
252+
vips::VOption *option = VImage::option()
253+
->set("shrink", shrink_on_load)
254+
->set("fail", baton->input->failOnError);
253255
if (baton->input->buffer != nullptr) {
254256
VipsBlob *blob = vips_blob_new(nullptr, baton->input->buffer, baton->input->bufferLength);
255257
if (inputImageType == ImageType::JPEG) {
713 Bytes
Loading

test/fixtures/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ module.exports = {
6666
inputJpgLarge: getPath('giant-image.jpg'),
6767
inputJpg320x240: getPath('320x240.jpg'), // http://www.andrewault.net/2010/01/26/create-a-test-pattern-video-with-perl/
6868
inputJpgOverlayLayer2: getPath('alpha-layer-2-ink.jpg'),
69+
inputJpgTruncated: getPath('truncated.jpg'), // head -c 10000 2569067123_aca715a2ee_o.jpg > truncated.jpg
6970

7071
inputPng: getPath('50020484-00001.png'), // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png
7172
inputPngWithTransparency: getPath('blackbug.png'), // public domain
@@ -81,6 +82,7 @@ module.exports = {
8182
inputPngAlphaPremultiplicationLarge: getPath('alpha-premultiply-2048x1536-paper.png'),
8283
inputPngBooleanNoAlpha: getPath('bandbool.png'),
8384
inputPngTestJoinChannel: getPath('testJoinChannel.png'),
85+
inputPngTruncated: getPath('truncated.png'), // gm convert 2569067123_aca715a2ee_o.jpg -resize 320x240 saw.png ; head -c 10000 saw.png > truncated.png
8486

8587
inputWebP: getPath('4.webp'), // http://www.gstatic.com/webp/gallery/4.webp
8688
inputWebPWithTransparency: getPath('5_webp_a.webp'), // http://www.gstatic.com/webp/gallery3/5_webp_a.webp

test/fixtures/truncated.jpg

9.77 KB
Loading

test/fixtures/truncated.png

9.77 KB
Loading

test/unit/failOnError.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
'use strict';
2+
3+
const assert = require('assert');
4+
5+
const sharp = require('../../');
6+
const fixtures = require('../fixtures');
7+
8+
describe('failOnError', function () {
9+
it('handles truncated JPEG by default', function (done) {
10+
sharp(fixtures.inputJpgTruncated)
11+
.resize(320, 240)
12+
// .toFile(fixtures.expected('truncated.jpg'), done);
13+
.toBuffer(function (err, data, info) {
14+
if (err) throw err;
15+
assert.strictEqual('jpeg', info.format);
16+
assert.strictEqual(320, info.width);
17+
assert.strictEqual(240, info.height);
18+
fixtures.assertSimilar(fixtures.expected('truncated.jpg'), data, done);
19+
});
20+
});
21+
22+
it('handles truncated PNG by default', function (done) {
23+
sharp(fixtures.inputPngTruncated)
24+
.resize(320, 240)
25+
// .toFile(fixtures.expected('truncated.png'), done);
26+
.toBuffer(function (err, data, info) {
27+
if (err) throw err;
28+
assert.strictEqual('png', info.format);
29+
assert.strictEqual(320, info.width);
30+
assert.strictEqual(240, info.height);
31+
done();
32+
});
33+
});
34+
35+
it('rejects invalid values', function () {
36+
assert.doesNotThrow(function () {
37+
sharp(fixtures.inputJpg, { failOnError: true });
38+
});
39+
40+
assert.throws(function () {
41+
sharp(fixtures.inputJpg, { failOnError: 'zoinks' });
42+
});
43+
44+
assert.throws(function () {
45+
sharp(fixtures.inputJpg, { failOnError: 1 });
46+
});
47+
});
48+
49+
it('returns errors to callback for truncated JPEG when failOnError is set', function (done) {
50+
sharp(fixtures.inputJpgTruncated, { failOnError: true }).toBuffer(function (err, data, info) {
51+
assert.ok(err.message.includes('VipsJpeg: Premature end of JPEG file'), err);
52+
assert.equal(data, null);
53+
assert.equal(info, null);
54+
done();
55+
});
56+
});
57+
58+
it('returns errors to callback for truncated PNG when failOnError is set', function (done) {
59+
sharp(fixtures.inputPngTruncated, { failOnError: true }).toBuffer(function (err, data, info) {
60+
assert.ok(err.message.includes('vipspng: libpng read error'), err);
61+
assert.equal(data, null);
62+
assert.equal(info, null);
63+
done();
64+
});
65+
});
66+
67+
it('rejects promises for truncated JPEG when failOnError is set', function (done) {
68+
sharp(fixtures.inputJpgTruncated, { failOnError: true })
69+
.toBuffer()
70+
.then(() => {
71+
throw new Error('Expected rejection');
72+
})
73+
.catch(err => {
74+
done(err.message.includes('VipsJpeg: Premature end of JPEG file') ? undefined : err);
75+
});
76+
});
77+
});

0 commit comments

Comments
 (0)