diff --git a/.clangd b/.clangd new file mode 100644 index 00000000..336c0f12 --- /dev/null +++ b/.clangd @@ -0,0 +1,5 @@ +# -*- yaml -*- + +CompileFlags: + Add: + - -Wno-c2y-extensions diff --git a/CHANGELOG.md b/CHANGELOG.md index abc3ca70..f1820317 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.27.0](#1-27-0) * [1.26.1](#1-26-1) * [1.26.0](#1-26-0) @@ -69,6 +70,56 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed + +* Do not allow codepoints to be merged into grapheme clusters directly + after a cursor move ([#2383][2383]). +* Dracula theme updated to latest official, and light theme alucard + added. +* Sixels: pan/pad clamped to 5 ([#2371][2371]). + +[2383]: https://codeberg.org/dnkl/foot/issues/2383 +[2371]: https://codeberg.org/dnkl/foot/issues/2371 + + +### Deprecated +### Removed +### Fixed + +* Out-of-bounds read when parsing URIs with malformed %-encoded + content ([#2353][2353]). +* DECCRA not clamping or verifying the destination rectangle + ([#2352][2352]). +* Empty selection clearing the clipboard ([#2327][2327]). +* Require xkbcommon >= 1.6.0. This has been the case for a while, due + to our use of `XKB_KEYSYM_MAX`. Now it is formalized in + `meson.build` ([#2379][2379]). +* Block selection area incorrectly updated when selecting back + across the starting column. +* Passing a very large value as CHT/CBT argument hangs the terminal + ([#2360][2360]). +* Sixel: crash when using a shared palette and gamma-correct blending + has been enabled, or foot is using 10-bit surface ([#2370][2370]). +* Kitty text-size protocol: fix crash when text is zero-length + ([#2364][2364]). +* Escape quotes in file names being DnD:ed on the command line + ([#2363][2363]). + +[2353]: https://codeberg.org/dnkl/foot/issues/2353 +[2352]: https://codeberg.org/dnkl/foot/issues/2352 +[2327]: https://codeberg.org/dnkl/foot/issues/2327 +[2379]: https://codeberg.org/dnkl/foot/issues/2379 +[2360]: https://codeberg.org/dnkl/foot/issues/2360 +[2370]: https://codeberg.org/dnkl/foot/issues/2370 +[2364]: https://codeberg.org/dnkl/foot/issues/2364 + + +### Security +### Contributors + + ## 1.27.0 ### Added diff --git a/csi.c b/csi.c index 87af215e..a82c0e2c 100644 --- a/csi.c +++ b/csi.c @@ -774,7 +774,7 @@ params_to_rectangular_area(const struct terminal *term, int first_idx, int rel_bottom = vt_param_get(term, first_idx + 2, term->rows) - 1; *right = min(vt_param_get(term, first_idx + 3, term->cols) - 1, term->cols - 1); - if (rel_top > rel_bottom || *left > *right) + if (unlikely(rel_top > rel_bottom || *left > *right)) return false; *top = term_row_rel_to_abs(term, rel_top); @@ -1171,37 +1171,40 @@ csi_dispatch(struct terminal *term, uint8_t final) case 'I': { /* CHT - Tab Forward (param is number of tab stops to move through) */ - for (int i = 0; i < vt_param_get(term, 0, 1); i++) { - int new_col = term->cols - 1; - tll_foreach(term->tab_stops, it) { - if (it->item > term->grid->cursor.point.col) { - new_col = it->item; + int count = vt_param_get(term, 0, 1); + int new_col = term->grid->cursor.point.col; + tll_foreach(term->tab_stops, it) { + if (it->item > new_col) { + if (--count < 0) { break; } + new_col = it->item; } - xassert(new_col >= term->grid->cursor.point.col); - - bool lcf = term->grid->cursor.lcf; - term_cursor_right(term, new_col - term->grid->cursor.point.col); - term->grid->cursor.lcf = lcf; } + xassert(new_col >= term->grid->cursor.point.col); + + bool lcf = term->grid->cursor.lcf; + term_cursor_right(term, new_col - term->grid->cursor.point.col); + term->grid->cursor.lcf = lcf; break; } - case 'Z': + case 'Z': { /* CBT - Back tab (param is number of tab stops to move back through) */ - for (int i = 0; i < vt_param_get(term, 0, 1); i++) { - int new_col = 0; - tll_rforeach(term->tab_stops, it) { - if (it->item < term->grid->cursor.point.col) { - new_col = it->item; + int count = vt_param_get(term, 0, 1); + int new_col = term->grid->cursor.point.col; + tll_rforeach(term->tab_stops, it) { + if (it->item < new_col) { + if (--count < 0) { break; } + new_col = it->item; } - xassert(term->grid->cursor.point.col >= new_col); - term_cursor_left(term, term->grid->cursor.point.col - new_col); } + xassert(term->grid->cursor.point.col >= new_col); + term_cursor_left(term, term->grid->cursor.point.col - new_col); break; + } case 'h': case 'l': { @@ -2005,9 +2008,8 @@ csi_dispatch(struct terminal *term, uint8_t final) } int src_page = vt_param_get(term, 4, 1); - int dst_rel_top = vt_param_get(term, 5, 1) - 1; - int dst_left = vt_param_get(term, 6, 1) - 1; + int dst_left = min(vt_param_get(term, 6, 1) - 1, term->cols - 1); int dst_page = vt_param_get(term, 7, 1); if (unlikely(src_page != 1 || dst_page != 1)) { @@ -2021,6 +2023,18 @@ csi_dispatch(struct terminal *term, uint8_t final) int dst_top = term_row_rel_to_abs(term, dst_rel_top); int dst_bottom = term_row_rel_to_abs(term, dst_rel_bottom); + if (unlikely(dst_left > dst_right || dst_top > dst_bottom)) + break; + + /* + * src validated by params_to_rectangular_area() + * dst validated above + */ + xassert(src_bottom - src_top >= 0); + xassert(dst_bottom - dst_top >= 0); + xassert(src_right - src_left >= 0); + xassert(dst_right - dst_left >= 0); + /* Target area outside the screen is clipped */ const size_t row_count = min(src_bottom - src_top, dst_bottom - dst_top) + 1; diff --git a/meson.build b/meson.build index e4e83302..12f2a514 100644 --- a/meson.build +++ b/meson.build @@ -151,7 +151,7 @@ wayland_protocols = dependency('wayland-protocols', version: '>=1.41', default_options: ['tests=false']) wayland_client = dependency('wayland-client') wayland_cursor = dependency('wayland-cursor') -xkb = dependency('xkbcommon', version: '>=1.0.0') +xkb = dependency('xkbcommon', version: '>=1.6.0') fontconfig = dependency('fontconfig') utf8proc = dependency('libutf8proc', required: get_option('grapheme-clustering')) diff --git a/osc.c b/osc.c index 95af5bff..05281d0e 100644 --- a/osc.c +++ b/osc.c @@ -1149,6 +1149,9 @@ kitty_text_size(struct terminal *term, char *string) *text = '\0'; text++; + if (text[0] == '\0') + return; + char32_t *wchars = ambstoc32(text); if (wchars == NULL) return; diff --git a/selection.c b/selection.c index 0a479ee8..dad81994 100644 --- a/selection.c +++ b/selection.c @@ -1145,7 +1145,7 @@ selection_update(struct terminal *term, int col, int row) struct coord *pivot_end = &term->selection.pivot.end; if (term->selection.kind == SELECTION_BLOCK) { - if (new_end.col > pivot_start->col) + if (new_end.col >= pivot_start->col) new_direction = SELECTION_RIGHT; else new_direction = SELECTION_LEFT; @@ -1948,6 +1948,9 @@ static const struct zwp_primary_selection_source_v1_listener primary_selection_s bool text_to_clipboard(struct seat *seat, struct terminal *term, char *text, uint32_t serial) { + if (text == NULL || text[0] == '\0') + return false; + xassert(serial != 0); struct wl_clipboard *clipboard = &seat->clipboard; @@ -2095,7 +2098,16 @@ decode_one_uri(struct clipboard_receive *ctx, char *uri, size_t len) if (ctx->quote_paths) ctx->cb("'", 1, ctx->user); - ctx->cb(path, strlen(path), ctx->user); + char *path_remaining = path; + for (char *next_quote = strchr(path_remaining, '\''); + next_quote != NULL; + path_remaining = next_quote + 1, + next_quote = strchr(path_remaining, '\'')) + { + ctx->cb(path_remaining, next_quote - path_remaining, ctx->user); + ctx->cb("\\'", 2, ctx->user); + } + ctx->cb(path_remaining, strlen(path_remaining), ctx->user); if (ctx->quote_paths) ctx->cb("'", 1, ctx->user); @@ -2418,6 +2430,9 @@ selection_from_clipboard(struct seat *seat, struct terminal *term, uint32_t seri bool text_to_primary(struct seat *seat, struct terminal *term, char *text, uint32_t serial) { + if (text == NULL || text[0] == '\0') + return false; + if (term->wl->primary_selection_device_manager == NULL) return false; diff --git a/sixel.c b/sixel.c index 187f1348..2bf47b7e 100644 --- a/sixel.c +++ b/sixel.c @@ -169,10 +169,10 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) if (term->sixel.linear_blending || term->sixel.use_10bit) { for (size_t i = 0; i < active_palette_entries; i++) { - uint8_t r = (term->sixel.private_palette[i] >> 16) & 0xff; - uint8_t g = (term->sixel.private_palette[i] >> 8) & 0xff; - uint8_t b = (term->sixel.private_palette[i] >> 0) & 0xff; - term->sixel.private_palette[i] = color_decode_srgb(term, r, g, b); + uint8_t r = (term->sixel.shared_palette[i] >> 16) & 0xff; + uint8_t g = (term->sixel.shared_palette[i] >> 8) & 0xff; + uint8_t b = (term->sixel.shared_palette[i] >> 0) & 0xff; + term->sixel.shared_palette[i] = color_decode_srgb(term, r, g, b); } } } else { @@ -1888,8 +1888,8 @@ decgra(struct terminal *term, uint8_t c) unsigned ph = nparams > 2 ? term->sixel.params[2] : 0; unsigned pv = nparams > 3 ? term->sixel.params[3] : 0; - pan = pan > 0 ? pan : 1; - pad = pad > 0 ? pad : 1; + pan = pan > 0 ? min(pan, 5) : 1; + pad = pad > 0 ? min(pad, 5) : 1; if (likely(term->sixel.image.width == 0 && term->sixel.image.height == 0)) diff --git a/terminal.c b/terminal.c index 8eafbcbe..374d4371 100644 --- a/terminal.c +++ b/terminal.c @@ -4086,6 +4086,10 @@ term_print(struct terminal *term, char32_t wc, int width, bool insert_mode_disab xassert(!grid->cursor.lcf); grid->cursor.point.col = col; + +#if defined(FOOT_GRAPHEME_CLUSTERING) + term->vt.codepoint_merging_ok = true; +#endif } static void @@ -4126,6 +4130,9 @@ ascii_printer_fast(struct terminal *term, char32_t wc) xassert(!grid->cursor.lcf); grid->cursor.point.col = col; +#if defined(FOOT_GRAPHEME_CLUSTERING) + term->vt.codepoint_merging_ok = true; +#endif if (unlikely(row->extra != NULL)) { grid_row_uri_range_erase(row, uri_start, uri_start); @@ -4210,7 +4217,11 @@ term_process_and_print_non_ascii(struct terminal *term, char32_t wc) { int width = c32width(wc); bool insert_mode_disable = false; - const bool grapheme_clustering = term->grapheme_shaping; + const bool grapheme_clustering = term->grapheme_shaping +#if defined(FOOT_GRAPHEME_CLUSTERING) + && term->vt.codepoint_merging_ok +#endif + ; #if !defined(FOOT_GRAPHEME_CLUSTERING) xassert(!grapheme_clustering); diff --git a/terminal.h b/terminal.h index 446d5f23..7e95697e 100644 --- a/terminal.h +++ b/terminal.h @@ -264,6 +264,7 @@ struct vt { char32_t last_printed; #if defined(FOOT_GRAPHEME_CLUSTERING) utf8proc_int32_t grapheme_state; + bool codepoint_merging_ok; #endif char32_t utf8; struct { @@ -994,5 +995,6 @@ static inline void term_reset_grapheme_state(struct terminal *term) { #if defined(FOOT_GRAPHEME_CLUSTERING) term->vt.grapheme_state = 0; + term->vt.codepoint_merging_ok = false; #endif } diff --git a/themes/ayu-dark b/themes/ayu-dark new file mode 100644 index 00000000..89d2896c --- /dev/null +++ b/themes/ayu-dark @@ -0,0 +1,26 @@ +# -*- conf -*- +# theme: Ayu Dark +# upstream: https://github.com/ayu-theme/ayu-colors/blob/master/themes/dark.yaml + +[colors-dark] +background=0d1017 +foreground=bfbdb6 +cursor=0d1017 ffb454 + +regular0=0a0000 # black +regular1=d75b63 # red +regular2=94c230 # green +regular3=e79e3a # yellow +regular4=40abe7 # blue +regular5=bc90e7 # magenta +regular6=7ecfb5 # cyan +regular7=ffffff # white + +bright0=0a0000 # bright black +bright1=f07178 # bright red +bright2=aad94c # bright green +bright3=ffb454 # bright yellow +bright4=59c2ff # bright blue +bright5=d2a6ff # bright magenta +bright6=95e6cb # bright cyan +bright7=ffffff # bright white diff --git a/themes/dracula b/themes/dracula index 82994203..93dd5f06 100644 --- a/themes/dracula +++ b/themes/dracula @@ -1,23 +1,43 @@ # -*- conf -*- -# Dracula +# Dracula / Alucard +# Source: https://github.com/dracula/foot [colors-dark] -cursor=282a36 f8f8f2 foreground=f8f8f2 background=282a36 -regular0=000000 # black -regular1=ff5555 # red -regular2=50fa7b # green -regular3=f1fa8c # yellow -regular4=bd93f9 # blue -regular5=ff79c6 # magenta -regular6=8be9fd # cyan -regular7=bfbfbf # white -bright0=4d4d4d # bright black -bright1=ff6e67 # bright red -bright2=5af78e # bright green -bright3=f4f99d # bright yellow -bright4=caa9fa # bright blue -bright5=ff92d0 # bright magenta -bright6=9aedfe # bright cyan -bright7=e6e6e6 # bright white \ No newline at end of file +regular0=21222c # black +regular1=ff5555 # red +regular2=50fa7b # green +regular3=f1fa8c # yellow +regular4=bd93f9 # blue +regular5=ff79c6 # magenta +regular6=8be9fd # cyan +regular7=f8f8f2 # white +bright0=6272a4 # bright black +bright1=ff6e6e # bright red +bright2=69ff94 # bright green +bright3=ffffa5 # bright yellow +bright4=d6acff # bright blue +bright5=ff92df # bright magenta +bright6=a4ffff # bright cyan +bright7=ffffff # bright white + +[colors-light] +foreground=1f1f1f +background=fffbeb +regular0=fffbeb # white +regular1=cb3a2a # red +regular2=14710a # green +regular3=846e15 # yellow +regular4=644ac9 # blue +regular5=a3144d # magenta +regular6=036a96 # cyan +regular7=1f1f1f # white +bright0=6c664b # bright black +bright1=d74c3d # bright red +bright2=198d0c # bright green +bright3=9e841a # bright yellow +bright4=7862d0 # bright blue +bright5=bf185a # bright magenta +bright6=047fb4 # bright cyan +bright7=2c2b31 # bright white diff --git a/uri.c b/uri.c index febe2e47..ddbfacdd 100644 --- a/uri.c +++ b/uri.c @@ -195,7 +195,9 @@ uri_parse(const char *uri, size_t len, encoded_len -= prefix_len; decoded_len += prefix_len; - if (hex2nibble(next[1]) <= 15 && hex2nibble(next[2]) <= 15) { + if (encoded_len >= 3 && + hex2nibble(next[1]) <= 15 && hex2nibble(next[2]) <= 15) + { *p++ = hex2nibble(next[1]) << 4 | hex2nibble(next[2]); decoded_len++; encoded_len -= 3; @@ -269,3 +271,177 @@ hostname_is_localhost(const char *hostname) streq(hostname, "localhost") || streq(hostname, this_host))); } + +UNITTEST +{ + /* Valid URI, all components */ + const char uri[] = "http://john:wick@www.foobar.com:80/this/is/the%20path?query1=abc&query2=def#fragment"; + + char *scheme, *user, *password, *host, *path, *query, *fragment; + uint16_t port = 0; + + uri_parse( + uri, sizeof(uri) - 1, &scheme, &user, &password, &host, &port, &path, + &query, &fragment); + xassert(streq(scheme, "http")); free(scheme); + xassert(streq(user, "john")); free(user); + xassert(streq(password, "wick")); free(password); + xassert(streq(host, "www.foobar.com")); free(host); + xassert(port == 80); + xassert(streq(path, "/this/is/the path")); free(path); + xassert(streq(query, "query1=abc&query2=def")); free(query); + xassert(streq(fragment, "fragment")); free(fragment); +} + +UNITTEST +{ + /* Valid URI, all components. Using file scheme, so query+fragment is treated as part of the path */ + const char uri[] = "file://john:wick@www.foobar.com:80/this/is/the%20path?query1=abc&query2=def#fragment"; + + char *scheme, *user, *password, *host, *path, *query, *fragment; + uint16_t port = 0; + + uri_parse( + uri, sizeof(uri) - 1, &scheme, &user, &password, &host, &port, &path, + &query, &fragment); + xassert(streq(scheme, "file")); free(scheme); + xassert(streq(user, "john")); free(user); + xassert(streq(password, "wick")); free(password); + xassert(streq(host, "www.foobar.com")); free(host); + xassert(port == 80); + xassert(streq(path, "/this/is/the path?query1=abc&query2=def#fragment")); free(path); + xassert(query == NULL); + xassert(fragment == NULL); +} + +UNITTEST +{ + /* Test a valid URI containing all components, except password */ + const char uri[] = "http://john@www.foobar.com:80/this/is/the%20path?query1=abc&query2=def#fragment"; + + char *scheme, *user, *password, *host, *path, *query, *fragment; + uint16_t port = 0; + + uri_parse( + uri, sizeof(uri) - 1, &scheme, &user, &password, &host, &port, &path, + &query, &fragment); + xassert(streq(scheme, "http")); free(scheme); + xassert(streq(user, "john")); free(user); + xassert(password == NULL); + xassert(streq(host, "www.foobar.com")); free(host); + xassert(port == 80); + xassert(streq(path, "/this/is/the path")); free(path); + xassert(streq(query, "query1=abc&query2=def")); free(query); + xassert(streq(fragment, "fragment")); free(fragment); +} + +UNITTEST +{ + /* Valid URI, all components, except user+password */ + const char uri[] = "http://www.foobar.com:80/this/is/the%20path?query1=abc&query2=def#fragment"; + + char *scheme, *user, *password, *host, *path, *query, *fragment; + uint16_t port = 0; + + uri_parse( + uri, sizeof(uri) - 1, &scheme, &user, &password, &host, &port, &path, + &query, &fragment); + xassert(streq(scheme, "http")); free(scheme); + xassert(user == NULL); + xassert(password == NULL); + xassert(streq(host, "www.foobar.com")); free(host); + xassert(port == 80); + xassert(streq(path, "/this/is/the path")); free(path); + xassert(streq(query, "query1=abc&query2=def")); free(query); + xassert(streq(fragment, "fragment")); free(fragment); +} + +UNITTEST +{ + /* Valid URI, fragment, no query */ + const char uri[] = "http://www.foobar.com:80/this/is/the%20path#fragment"; + + char *scheme, *user, *password, *host, *path, *query, *fragment; + uint16_t port = 0; + + uri_parse( + uri, sizeof(uri) - 1, &scheme, &user, &password, &host, &port, &path, + &query, &fragment); + xassert(streq(scheme, "http")); free(scheme); + xassert(user == NULL); + xassert(password == NULL); + xassert(streq(host, "www.foobar.com")); free(host); + xassert(port == 80); + xassert(streq(path, "/this/is/the path")); free(path); + xassert(query == NULL); + xassert(streq(fragment, "fragment")); free(fragment); +} + +UNITTEST +{ + /* Valid URI, query, no fragment */ + const char uri[] = "http://www.foobar.com:80/this/is/the%20path?query1=abc&query2=def"; + + char *scheme, *user, *password, *host, *path, *query, *fragment; + uint16_t port = 0; + + uri_parse( + uri, sizeof(uri) - 1, &scheme, &user, &password, &host, &port, &path, + &query, &fragment); + xassert(streq(scheme, "http")); free(scheme); + xassert(user == NULL); + xassert(password == NULL); + xassert(streq(host, "www.foobar.com")); free(host); + xassert(port == 80); + xassert(streq(path, "/this/is/the path")); free(path); + xassert(streq(query, "query1=abc&query2=def")); free(query); + xassert(fragment == NULL); +} + +UNITTEST +{ + /* Malformed URI, fragment before query (treated as part of path instead) */ + const char uri[] = "http://www.foobar.com:80/this/is/the%20path#fragment?query1=abc&query2=def"; + + char *scheme, *user, *password, *host, *path, *query, *fragment; + uint16_t port = 0; + + uri_parse( + uri, sizeof(uri) - 1, &scheme, &user, &password, &host, &port, &path, + &query, &fragment); + xassert(streq(scheme, "http")); free(scheme); + xassert(user == NULL); + xassert(password == NULL); + xassert(streq(host, "www.foobar.com")); free(host); + xassert(port == 80); + xassert(streq(path, "/this/is/the path#fragment?query1=abc&query2=def")); free(path); + xassert(query == NULL); + xassert(fragment == NULL); +} + +UNITTEST +{ + /* Malformed URI, trailing '%' */ + const char uri[] = "file:///%ABNOT-PART-OF-INPUT"; + char *path; + uri_parse(uri, 9, NULL, NULL, NULL, NULL, NULL, &path, NULL, NULL); + xassert(streq(path, "/%")); free(path); +} + +UNITTEST +{ + /* Malformed URI, trailing '%2' */ + const char uri[] = "file:///%2ANOT-PART-OF-INPUT"; + char *path; + uri_parse(uri, 10, NULL, NULL, NULL, NULL, NULL, &path, NULL, NULL); + xassert(streq(path, "/%2")); free(path); +} + +UNITTEST +{ + /* Malformed URI, trailing '%ag' */ + const char uri[] = "file:///%ag"; + char *path; + uri_parse(uri, sizeof(uri) - 1, NULL, NULL, NULL, NULL, NULL, &path, NULL, NULL); + xassert(streq(path, "/%ag")); free(path); +}