Skip to content

Commit

Permalink
Check magic bytes before passing a file to Imlib2's loader
Browse files Browse the repository at this point in the history
This works around a regression in Imlib2, which makes (un)loadable file
detection quite slow when handling e.g. large video files. See
<https://phab.enlightenment.org/T8739> and
<#505> for details.

Closes #505
  • Loading branch information
derf committed Nov 30, 2020
1 parent 2e4fc90 commit 2d37cd9
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 4 deletions.
17 changes: 17 additions & 0 deletions man/feh.pre
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,23 @@ Use
.Cm --conversion-timeout Ar timeout
with a non-negative value to enable support for these formats.
.
.Pp
.
As Imlib2 may take several seconds to determine whether it can load a file or
not
.Pq e.g. when attempting to open a large video ,
.Nm
checks each file's header before loading it.
If it looks like an image, it is passed on to Imlib2, otherwise, it is
assumed to be unloadable.
This greatly improves performance when working in directories with mixed files
.Pq i.e., directories which do not exclusively contain image files .
If you think that Imlib2 can load a file which
.Nm
has determined to be likely not an image, set the environment variable
.Qq FEH_SKIP_MAGIC
to pass all files directly to Imlib2, bypassing the header check.
The environment variable's value does not matter, it just needs to be set.
.
.Sh OPTIONS
.
Expand Down
103 changes: 100 additions & 3 deletions src/imlib.c
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,11 @@ void feh_imlib_print_load_error(char *file, winwidget w, Imlib_Load_Error err)
break;
case IMLIB_LOAD_ERROR_UNKNOWN:
case IMLIB_LOAD_ERROR_NO_LOADER_FOR_FILE_FORMAT:
im_weprintf(w, "%s - No Imlib2 loader for that file format", file);
if (getenv("FEH_SKIP_MAGIC")) {
im_weprintf(w, "%s - No Imlib2 loader for that file format", file);
} else {
im_weprintf(w, "%s - Does not look like an image (magic bytes missing)", file);
}
break;
case IMLIB_LOAD_ERROR_PATH_TOO_LONG:
im_weprintf(w, "%s - Path specified is too long", file);
Expand Down Expand Up @@ -215,6 +219,94 @@ void feh_imlib_print_load_error(char *file, winwidget w, Imlib_Load_Error err)
}
}

/*
* This is a workaround for an Imlib2 regression, causing unloadable image
* detection to be excessively slow (and, thus, causing feh to hang for a while
* when encountering an unloadable image). We use magic byte detection to
* avoid calling Imlib2 for files it probably cannot handle. See
* <https://phab.enlightenment.org/T8739> and
* <https://github.com/derf/feh/issues/505>.
*
* Note that this drops support for bz2-compressed files, unless
* FEH_SKIP_MAGIC is set
*/
int feh_is_image(feh_file * file)
{
unsigned char buf[16];
FILE *fh = fopen(file->filename, "r");
if (!fh) {
return 0;
}
if (fread(buf, 1, 16, fh) != 16) {
return 0;
}
fclose(fh);

if (buf[0] == 0xff && buf[1] == 0xd8) {
// JPEG
return 1;
}
if (!memcmp(buf, "\x89PNG\x0d\x0a\x1a\x0a", 8)) {
// PNG
return 1;
}
if (buf[0] == 'A' && buf[1] == 'R' && buf[2] == 'G' && buf[3] == 'B') {
// ARGB
return 1;
}
if (buf[0] == 'B' && buf[1] == 'M') {
// BMP
return 1;
}
if (!memcmp(buf, "farbfeld", 8)) {
// farbfeld
return 1;
}
if (buf[0] == 'G' && buf[1] == 'I' && buf[2] == 'F') {
// GIF
return 1;
}
if (buf[0] == 0x00 && buf[1] == 0x00 && buf[2] <= 0x02 && buf[3] == 0x00) {
// ICO
return 1;
}
if (!memcmp(buf, "FORM", 4)) {
// Amiga IFF ILBM
return 1;
}
if (buf[0] == 'P' && buf[1] >= '1' && buf[1] <= '7') {
// PNM et al.
return 1;
}
if (strstr(file->filename, ".tga")) {
// TGA
return 1;
}
if (!memcmp(buf, "II\x2a\x00", 4) || !memcmp(buf, "MM\x00\x2a", 4)) {
// TIFF
return 1;
}
if (!memcmp(buf, "RIFF", 4)) {
// might be webp
return 1;
}
buf[15] = 0;
if (strstr((char *)buf, "XPM")) {
// XPM
return 1;
}
if (strstr(file->filename, ".bz2") || strstr(file->filename, ".gz")) {
// Imlib2 supports compressed images. It relies on the filename to
// determine the appropriate loader and does not use magic bytes here.
return 1;
}
// moved to the end as this variable won't be set in most cases
if (getenv("FEH_SKIP_MAGIC")) {
return 1;
}
return 0;
}

int feh_load_image(Imlib_Image * im, feh_file * file)
{
Imlib_Load_Error err = IMLIB_LOAD_ERROR_NONE;
Expand All @@ -239,8 +331,13 @@ int feh_load_image(Imlib_Image * im, feh_file * file)
if (!tmpname)
err = IMLIB_LOAD_ERROR_NO_LOADER_FOR_FILE_FORMAT;
}
else
*im = imlib_load_image_with_error_return(file->filename, &err);
else {
if (feh_is_image(file)) {
*im = imlib_load_image_with_error_return(file->filename, &err);
} else {
err = IMLIB_LOAD_ERROR_NO_LOADER_FOR_FILE_FORMAT;
}
}

if (opt.conversion_timeout >= 0 && (
(err == IMLIB_LOAD_ERROR_UNKNOWN) ||
Expand Down
2 changes: 1 addition & 1 deletion test/feh.t
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ if ( $version =~ m{ Compile-time \s switches : \s .* help }ox ) {
}

my $re_warning
= qr{${feh_name} WARNING: test/fail/... \- No Imlib2 loader for that file format\n};
= qr{${feh_name} WARNING: test/fail/... \- Does not look like an image \(magic bytes missing\)\n};
my $re_loadable = qr{test/ok/...};
my $re_unloadable = qr{test/fail/...};
my $re_list_action = qr{test/ok/... 16x16};
Expand Down

0 comments on commit 2d37cd9

Please sign in to comment.