Skip to content

Commit c8b1ec4

Browse files
author
oxidworks
committed
Prevent UI freeze when opening binary files
Binary files typically contain no newline characters, causing the entire file content to be placed in a single GtkTextBuffer line. This triggers extremely slow Pango/HarfBuzz text shaping (O(n²) for varied content) that blocks the GTK main thread indefinitely at 100% CPU. The problem is compounded by the encoding fallback mechanism which converts each byte to a 3-character hex escape (\XX), tripling the line length. This commit adds three mitigations: 1. Pre-load binary detection in _xed_tab_load(): Before starting the file loader, check the content type via GIO and scan the first 8KB for NUL bytes. If the file appears binary, show a warning info bar with "Open Anyway" / "Don't Open" buttons instead of loading immediately. Text-like types (text/*, XML, JSON, JS, SVG) are always allowed through. 2. New xed_binary_file_warning_info_bar_new(): Creates a warning info bar showing the detected file type description and explaining that opening binary files can cause unresponsiveness. 3. Cursor position optimization in update_cursor_position_statusbar(): For lines longer than 10,000 characters, use gtk_text_iter_get_line_offset() directly instead of the O(n) character-by-character tab-width calculation loop. This prevents a secondary freeze vector when the cursor moves on very long lines. Fixes: #491 Fixes: #591 Related: #624
1 parent 78ff0d1 commit c8b1ec4

4 files changed

Lines changed: 212 additions & 9 deletions

File tree

xed/xed-io-error-info-bar.c

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1136,3 +1136,61 @@ xed_invalid_character_info_bar_new (GFile *location)
11361136

11371137
return info_bar;
11381138
}
1139+
1140+
GtkWidget *
1141+
xed_binary_file_warning_info_bar_new (GFile *location,
1142+
const gchar *content_type)
1143+
{
1144+
GtkWidget *info_bar;
1145+
gchar *full_formatted_uri;
1146+
gchar *temp_uri_for_display;
1147+
gchar *uri_for_display;
1148+
gchar *primary_text;
1149+
gchar *secondary_text;
1150+
1151+
g_return_val_if_fail (G_IS_FILE (location), NULL);
1152+
1153+
full_formatted_uri = g_file_get_parse_name (location);
1154+
temp_uri_for_display = xed_utils_str_middle_truncate (full_formatted_uri, MAX_URI_IN_DIALOG_LENGTH);
1155+
g_free (full_formatted_uri);
1156+
1157+
uri_for_display = g_markup_printf_escaped ("<i>%s</i>", temp_uri_for_display);
1158+
g_free (temp_uri_for_display);
1159+
1160+
info_bar = gtk_info_bar_new ();
1161+
1162+
gtk_info_bar_add_button (GTK_INFO_BAR (info_bar),
1163+
_("Open Any_way"),
1164+
GTK_RESPONSE_YES);
1165+
gtk_info_bar_add_button (GTK_INFO_BAR (info_bar),
1166+
_("D_on't Open"),
1167+
GTK_RESPONSE_CANCEL);
1168+
gtk_info_bar_set_message_type (GTK_INFO_BAR (info_bar), GTK_MESSAGE_WARNING);
1169+
1170+
primary_text = g_strdup_printf (_("The file %s appears to be a binary file."), uri_for_display);
1171+
g_free (uri_for_display);
1172+
1173+
if (content_type != NULL)
1174+
{
1175+
gchar *desc = g_content_type_get_description (content_type);
1176+
1177+
secondary_text = g_strdup_printf (
1178+
_("The file type is \"%s\". Opening binary files in a text editor "
1179+
"can cause the application to become unresponsive."),
1180+
desc);
1181+
g_free (desc);
1182+
}
1183+
else
1184+
{
1185+
secondary_text = g_strdup (
1186+
_("Opening binary files in a text editor can cause the application "
1187+
"to become unresponsive."));
1188+
}
1189+
1190+
set_info_bar_text_and_icon (info_bar, "xsi-dialog-warning-symbolic",
1191+
primary_text, secondary_text);
1192+
g_free (primary_text);
1193+
g_free (secondary_text);
1194+
1195+
return info_bar;
1196+
}

xed/xed-io-error-info-bar.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ GtkWidget *xed_externally_modified_info_bar_new (GFile *location,
6464

6565
GtkWidget *xed_invalid_character_info_bar_new (GFile *location);
6666

67+
GtkWidget *xed_binary_file_warning_info_bar_new (GFile *location,
68+
const gchar *content_type);
69+
6770
G_END_DECLS
6871

6972
#endif /* __XED_IO_ERROR_INFO_BAR_H__ */

xed/xed-tab.c

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ struct _XedTabPrivate
8383

8484
/*tmp data for loading */
8585
guint user_requested_encoding : 1;
86+
87+
/* Set after user confirms opening a binary file */
88+
guint binary_check_done : 1;
8689
};
8790

8891
typedef struct _SaverData SaverData;
@@ -1961,6 +1964,106 @@ load (XedTab *tab,
19611964
tab);
19621965
}
19631966

1967+
/* Check if a file appears to be binary by reading the first chunk and
1968+
* looking for NUL bytes, which are almost never present in text files.
1969+
* Also checks the content type via GIO.
1970+
* Returns the content type string (caller must free) or NULL if text.
1971+
*/
1972+
static gchar *
1973+
check_file_is_binary (GFile *location)
1974+
{
1975+
GFileInfo *info;
1976+
const gchar *content_type;
1977+
GFileInputStream *stream;
1978+
guchar buf[8192];
1979+
gssize bytes_read;
1980+
gssize i;
1981+
1982+
info = g_file_query_info (location,
1983+
G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
1984+
G_FILE_QUERY_INFO_NONE,
1985+
NULL,
1986+
NULL);
1987+
1988+
if (info != NULL)
1989+
{
1990+
content_type = g_file_info_get_content_type (info);
1991+
1992+
if (content_type != NULL &&
1993+
!g_content_type_is_unknown (content_type) &&
1994+
!g_content_type_is_a (content_type, "text/plain") &&
1995+
!g_content_type_equals (content_type, "application/xml") &&
1996+
!g_content_type_is_a (content_type, "application/xml") &&
1997+
!g_content_type_equals (content_type, "application/json") &&
1998+
!g_content_type_equals (content_type, "application/javascript") &&
1999+
!g_content_type_equals (content_type, "image/svg+xml"))
2000+
{
2001+
gchar *ret = g_strdup (content_type);
2002+
g_object_unref (info);
2003+
return ret;
2004+
}
2005+
2006+
g_object_unref (info);
2007+
}
2008+
2009+
/* Fallback: read the first 8KB and check for NUL bytes */
2010+
stream = g_file_read (location, NULL, NULL);
2011+
2012+
if (stream == NULL)
2013+
{
2014+
return NULL;
2015+
}
2016+
2017+
bytes_read = g_input_stream_read (G_INPUT_STREAM (stream),
2018+
buf,
2019+
sizeof (buf),
2020+
NULL,
2021+
NULL);
2022+
g_object_unref (stream);
2023+
2024+
if (bytes_read <= 0)
2025+
{
2026+
return NULL;
2027+
}
2028+
2029+
for (i = 0; i < bytes_read; i++)
2030+
{
2031+
if (buf[i] == '\0')
2032+
{
2033+
return g_strdup ("application/octet-stream");
2034+
}
2035+
}
2036+
2037+
return NULL;
2038+
}
2039+
2040+
static void
2041+
binary_file_warning_info_bar_response (GtkWidget *info_bar,
2042+
gint response_id,
2043+
XedTab *tab)
2044+
{
2045+
GFile *location;
2046+
GtkSourceFile *file;
2047+
XedDocument *doc;
2048+
2049+
doc = xed_tab_get_document (tab);
2050+
file = xed_document_get_file (doc);
2051+
location = gtk_source_file_get_location (file);
2052+
2053+
set_info_bar (tab, NULL);
2054+
2055+
if (response_id == GTK_RESPONSE_YES)
2056+
{
2057+
/* User chose "Open Anyway" — skip binary check and load */
2058+
tab->priv->binary_check_done = TRUE;
2059+
_xed_tab_load (tab, location, NULL, 0, FALSE);
2060+
}
2061+
else
2062+
{
2063+
remove_tab (tab);
2064+
}
2065+
}
2066+
19642067
void
19652068
_xed_tab_load (XedTab *tab,
19662069
GFile *location,
@@ -1975,6 +2078,33 @@ _xed_tab_load (XedTab *tab,
19752078
g_return_if_fail (G_IS_FILE (location));
19762079
g_return_if_fail (tab->priv->state == XED_TAB_STATE_NORMAL);
19772080

2081+
/* Check for binary files before loading to prevent UI freeze.
2082+
* Binary files without line breaks cause Pango text shaping to
2083+
* block the main thread indefinitely (see issue #491, #591).
2084+
*/
2085+
if (!tab->priv->binary_check_done && !create)
2086+
{
2087+
gchar *binary_type = check_file_is_binary (location);
2088+
2089+
if (binary_type != NULL)
2090+
{
2091+
GtkWidget *info_bar;
2092+
2093+
info_bar = xed_binary_file_warning_info_bar_new (location, binary_type);
2094+
g_free (binary_type);
2095+
2096+
g_signal_connect (info_bar, "response",
2097+
G_CALLBACK (binary_file_warning_info_bar_response), tab);
2098+
2099+
set_info_bar (tab, info_bar);
2100+
gtk_info_bar_set_default_response (GTK_INFO_BAR (info_bar), GTK_RESPONSE_CANCEL);
2101+
gtk_widget_show (info_bar);
2102+
return;
2103+
}
2104+
}
2105+
2106+
tab->priv->binary_check_done = FALSE;
2107+
19782108
xed_tab_set_state (tab, XED_TAB_STATE_LOADING);
19792109

19802110
doc = xed_tab_get_document (tab);

xed/xed-window.c

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1791,18 +1791,30 @@ update_cursor_position_statusbar (GtkTextBuffer *buffer,
17911791

17921792
tab_size = gtk_source_view_get_tab_width (GTK_SOURCE_VIEW(view));
17931793

1794-
while (!gtk_text_iter_equal (&start, &iter))
1794+
/* For very long lines (e.g. binary files opened as text), the
1795+
* character-by-character iteration below becomes extremely slow
1796+
* and can freeze the UI. Use the offset directly if the line is
1797+
* too long for the O(n) scan to be practical.
1798+
*/
1799+
if (gtk_text_iter_get_chars_in_line (&start) > 10000)
17951800
{
1796-
/* FIXME: Are we Unicode compliant here? */
1797-
if (gtk_text_iter_get_char (&start) == '\t')
1798-
{
1799-
col += (tab_size - (col % tab_size));
1800-
}
1801-
else
1801+
col = gtk_text_iter_get_line_offset (&iter);
1802+
}
1803+
else
1804+
{
1805+
while (!gtk_text_iter_equal (&start, &iter))
18021806
{
1803-
++col;
1807+
/* FIXME: Are we Unicode compliant here? */
1808+
if (gtk_text_iter_get_char (&start) == '\t')
1809+
{
1810+
col += (tab_size - (col % tab_size));
1811+
}
1812+
else
1813+
{
1814+
++col;
1815+
}
1816+
gtk_text_iter_forward_char (&start);
18041817
}
1805-
gtk_text_iter_forward_char (&start);
18061818
}
18071819

18081820
xed_statusbar_set_cursor_position (XED_STATUSBAR(window->priv->statusbar), row + 1, col + 1);

0 commit comments

Comments
 (0)