Fix prediction unicode bug. Make all Cell members private.

Fixes #702.
This commit is contained in:
John Hood
2015-12-21 19:13:43 -05:00
parent 90a529b18a
commit 2ac3bbeb9b
11 changed files with 226 additions and 49 deletions
+23 -23
View File
@@ -61,7 +61,7 @@ void ConditionalOverlayCell::apply( Framebuffer &fb, uint64_t confirmed_epoch, i
if ( unknown ) { if ( unknown ) {
if ( flag && ( col != fb.ds.get_width() - 1 ) ) { if ( flag && ( col != fb.ds.get_width() - 1 ) ) {
fb.get_mutable_cell( row, col )->renditions.set_attribute(Renditions::underlined, true); fb.get_mutable_cell( row, col )->get_renditions().set_attribute(Renditions::underlined, true);
} }
return; return;
} }
@@ -69,7 +69,7 @@ void ConditionalOverlayCell::apply( Framebuffer &fb, uint64_t confirmed_epoch, i
if ( *fb.get_cell( row, col ) != replacement ) { if ( *fb.get_cell( row, col ) != replacement ) {
*(fb.get_mutable_cell( row, col )) = replacement; *(fb.get_mutable_cell( row, col )) = replacement;
if ( flag ) { if ( flag ) {
fb.get_mutable_cell( row, col )->renditions.set_attribute( Renditions::underlined, true ); fb.get_mutable_cell( row, col )->get_renditions().set_attribute( Renditions::underlined, true );
} }
} }
} }
@@ -206,9 +206,9 @@ void NotificationEngine::apply( Framebuffer &fb ) const
/* draw bar across top of screen */ /* draw bar across top of screen */
Cell notification_bar( 0 ); Cell notification_bar( 0 );
notification_bar.renditions.foreground_color = 37; notification_bar.get_renditions().foreground_color = 37;
notification_bar.renditions.background_color = 44; notification_bar.get_renditions().background_color = 44;
notification_bar.contents.push_back( 0x20 ); notification_bar.append( 0x20 );
for ( int i = 0; i < fb.ds.get_width(); i++ ) { for ( int i = 0; i < fb.ds.get_width(); i++ ) {
*(fb.get_mutable_cell( 0, i )) = notification_bar; *(fb.get_mutable_cell( 0, i )) = notification_bar;
@@ -275,12 +275,12 @@ void NotificationEngine::apply( Framebuffer &fb ) const
case 2: /* wide character */ case 2: /* wide character */
this_cell = fb.get_mutable_cell( 0, overlay_col ); this_cell = fb.get_mutable_cell( 0, overlay_col );
fb.reset_cell( this_cell ); fb.reset_cell( this_cell );
this_cell->renditions.set_attribute(Renditions::bold, true); this_cell->get_renditions().set_attribute(Renditions::bold, true);
this_cell->renditions.foreground_color = 37; this_cell->get_renditions().foreground_color = 37;
this_cell->renditions.background_color = 44; this_cell->get_renditions().background_color = 44;
this_cell->contents.push_back( ch ); this_cell->append( ch );
this_cell->width = chwidth; this_cell->set_width( chwidth );
combining_cell = this_cell; combining_cell = this_cell;
overlay_col += chwidth; overlay_col += chwidth;
@@ -290,14 +290,14 @@ void NotificationEngine::apply( Framebuffer &fb ) const
break; break;
} }
if ( combining_cell->contents.size() == 0 ) { if ( combining_cell->empty() ) {
assert( combining_cell->width == 1 ); assert( combining_cell->get_width() == 1 );
combining_cell->fallback = true; combining_cell->set_fallback( true );
overlay_col++; overlay_col++;
} }
if ( combining_cell->contents.size() < 32 ) { if ( !combining_cell->full() ) {
combining_cell->contents.push_back( ch ); combining_cell->append( ch );
} }
break; break;
case -1: /* unprintable character */ case -1: /* unprintable character */
@@ -560,11 +560,11 @@ void PredictionEngine::cull( const Framebuffer &fb )
/* match rest of row to the actual renditions */ /* match rest of row to the actual renditions */
{ {
const Renditions &actual_renditions = fb.get_cell( i->row_num, j->col )->renditions; const Renditions &actual_renditions = fb.get_cell( i->row_num, j->col )->get_renditions();
for ( overlay_cells_type::iterator k = j; for ( overlay_cells_type::iterator k = j;
k != i->overlay_cells.end(); k != i->overlay_cells.end();
k++ ) { k++ ) {
k->replacement.renditions = actual_renditions; k->replacement.get_renditions() = actual_renditions;
} }
} }
@@ -777,21 +777,21 @@ void PredictionEngine::new_user_byte( char the_byte, const Framebuffer &fb )
cell.active = true; cell.active = true;
cell.tentative_until_epoch = prediction_epoch; cell.tentative_until_epoch = prediction_epoch;
cell.expire( local_frame_sent + 1, now ); cell.expire( local_frame_sent + 1, now );
cell.replacement.renditions = fb.ds.get_renditions(); cell.replacement.get_renditions() = fb.ds.get_renditions();
/* heuristic: match renditions of character to the left */ /* heuristic: match renditions of character to the left */
if ( cursor().col > 0 ) { if ( cursor().col > 0 ) {
ConditionalOverlayCell &prev_cell = the_row.overlay_cells[ cursor().col - 1 ]; ConditionalOverlayCell &prev_cell = the_row.overlay_cells[ cursor().col - 1 ];
const Cell *prev_cell_actual = fb.get_cell( cursor().row, cursor().col - 1 ); const Cell *prev_cell_actual = fb.get_cell( cursor().row, cursor().col - 1 );
if ( prev_cell.active && (!prev_cell.unknown) ) { if ( prev_cell.active && (!prev_cell.unknown) ) {
cell.replacement.renditions = prev_cell.replacement.renditions; cell.replacement.get_renditions() = prev_cell.replacement.get_renditions();
} else { } else {
cell.replacement.renditions = prev_cell_actual->renditions; cell.replacement.get_renditions() = prev_cell_actual->get_renditions();
} }
} }
cell.replacement.contents.clear(); cell.replacement.clear();
cell.replacement.contents.push_back( ch ); cell.replacement.append( ch );
cell.original_contents.push_back( *fb.get_cell( cursor().row, cursor().col ) ); cell.original_contents.push_back( *fb.get_cell( cursor().row, cursor().col ) );
/* /*
@@ -876,7 +876,7 @@ void PredictionEngine::newline_carriage_return( const Framebuffer &fb )
j->active = true; j->active = true;
j->tentative_until_epoch = prediction_epoch; j->tentative_until_epoch = prediction_epoch;
j->expire( local_frame_sent + 1, now ); j->expire( local_frame_sent + 1, now );
j->replacement.contents.clear(); j->replacement.clear();
} }
} else { } else {
cursor().row++; cursor().row++;
+5 -6
View File
@@ -107,7 +107,7 @@ void Emulator::print( const Parser::Print *act )
fb.reset_cell( this_cell ); fb.reset_cell( this_cell );
this_cell->append( ch ); this_cell->append( ch );
this_cell->width = chwidth; this_cell->set_width( chwidth );
fb.apply_renditions_to_cell( this_cell ); fb.apply_renditions_to_cell( this_cell );
if ( chwidth == 2 ) { /* erase overlapped cell */ if ( chwidth == 2 ) { /* erase overlapped cell */
@@ -128,18 +128,17 @@ void Emulator::print( const Parser::Print *act )
break; break;
} }
if ( combining_cell->contents.size() == 0 ) { if ( combining_cell->empty() ) {
/* cell starts with combining character */ /* cell starts with combining character */
/* ... but isn't necessarily the target for a new /* ... but isn't necessarily the target for a new
base character [e.g. start of line], if the base character [e.g. start of line], if the
combining character has been cleared with combining character has been cleared with
a sequence like ED ("J") or EL ("K") */ a sequence like ED ("J") or EL ("K") */
assert( combining_cell->width == 1 ); assert( combining_cell->get_width() == 1 );
combining_cell->fallback = true; combining_cell->set_fallback( true );
fb.ds.move_col( 1, true, true ); fb.ds.move_col( 1, true, true );
} }
if ( combining_cell->contents.size() < 32 ) { if ( !combining_cell->full() ) {
/* seems like a reasonable limit on combining characters */
combining_cell->append( ch ); combining_cell->append( ch );
} }
act->handled = true; act->handled = true;
+12 -12
View File
@@ -337,10 +337,10 @@ bool Display::put_row( bool initialized, FrameState &frame, const Framebuffer &f
/* If we're forced to write the first column because of wrap, go ahead and do so. */ /* If we're forced to write the first column because of wrap, go ahead and do so. */
if ( wrap ) { if ( wrap ) {
const Cell &cell = cells.at( 0 ); const Cell &cell = cells.at( 0 );
frame.update_rendition( cell.renditions ); frame.update_rendition( cell.get_renditions() );
frame.append_cell( cell ); frame.append_cell( cell );
frame_x += cell.width; frame_x += cell.get_width();
frame.cursor_x += cell.width; frame.cursor_x += cell.get_width();
} }
/* If rows are the same object, we don't need to do anything at all. */ /* If rows are the same object, we don't need to do anything at all. */
@@ -361,16 +361,16 @@ bool Display::put_row( bool initialized, FrameState &frame, const Framebuffer &f
if ( initialized if ( initialized
&& !clear_count && !clear_count
&& ( cell == old_cells.at( frame_x ) ) ) { && ( cell == old_cells.at( frame_x ) ) ) {
frame_x += cell.width; frame_x += cell.get_width();
continue; continue;
} }
/* Slurp up all the empty cells */ /* Slurp up all the empty cells */
if ( cell.contents.empty() ) { if ( cell.empty() ) {
if ( !clear_count ) { if ( !clear_count ) {
blank_renditions = cell.renditions; blank_renditions = cell.get_renditions();
} }
if ( cell.renditions == blank_renditions ) { if ( cell.get_renditions() == blank_renditions ) {
/* Remember run of blank cells */ /* Remember run of blank cells */
clear_count++; clear_count++;
frame_x++; frame_x++;
@@ -394,8 +394,8 @@ bool Display::put_row( bool initialized, FrameState &frame, const Framebuffer &f
clear_count = 0; clear_count = 0;
// If the current character is *another* empty cell in a different rendition, // If the current character is *another* empty cell in a different rendition,
// we restart counting and continue here // we restart counting and continue here
if ( cell.contents.empty() ) { if ( cell.empty() ) {
blank_renditions = cell.renditions; blank_renditions = cell.get_renditions();
clear_count = 1; clear_count = 1;
frame_x++; frame_x++;
continue; continue;
@@ -406,10 +406,10 @@ bool Display::put_row( bool initialized, FrameState &frame, const Framebuffer &f
/* Now draw a character cell. */ /* Now draw a character cell. */
/* Move to the right position. */ /* Move to the right position. */
frame.append_silent_move( frame_y, frame_x ); frame.append_silent_move( frame_y, frame_x );
frame.update_rendition( cell.renditions ); frame.update_rendition( cell.get_renditions() );
frame.append_cell( cell ); frame.append_cell( cell );
frame_x += cell.width; frame_x += cell.get_width();
frame.cursor_x += cell.width; frame.cursor_x += cell.get_width();
if ( frame_x >= f.ds.get_width() ) { if ( frame_x >= f.ds.get_width() ) {
wrote_last_cell = true; wrote_last_cell = true;
} }
-1
View File
@@ -53,7 +53,6 @@ namespace Terminal {
void append( const size_t s, const char c ) { str.append( s, c ); } void append( const size_t s, const char c ) { str.append( s, c ); }
void append( const wchar_t wc ) { Cell::append_to_str( str, wc ); } void append( const wchar_t wc ) { Cell::append_to_str( str, wc ); }
void append( const char * s ) { str.append( s ); } void append( const char * s ) { str.append( s ); }
void append( const Cell::content_type &contents ) { str.append( contents.begin(), contents.end() ); }
void append_string( const std::string &append ) { str.append(append); } void append_string( const std::string &append ) { str.append(append); }
void append_cell(const Cell & cell) { cell.print_grapheme( str ); } void append_cell(const Cell & cell) { cell.print_grapheme( str ); }
+1 -1
View File
@@ -270,7 +270,7 @@ void Framebuffer::apply_renditions_to_cell( Cell *cell )
if (!cell) { if (!cell) {
cell = get_mutable_cell(); cell = get_mutable_cell();
} }
cell->renditions = ds.get_renditions(); cell->set_renditions( ds.get_renditions() );
} }
SavedCursor::SavedCursor() SavedCursor::SavedCursor()
+19 -2
View File
@@ -85,13 +85,14 @@ namespace Terminal {
}; };
class Cell { class Cell {
public: private:
typedef std::string content_type; /* can be std::string, std::vector<uint8_t>, or __gnu_cxx::__vstring */ typedef std::string content_type; /* can be std::string, std::vector<uint8_t>, or __gnu_cxx::__vstring */
content_type contents; content_type contents;
Renditions renditions; Renditions renditions;
uint8_t width; uint8_t width;
bool fallback; /* first character is combining character */ bool fallback; /* first character is combining character */
public:
Cell( color_type background_color ); Cell( color_type background_color );
Cell(); /* default constructor required by C++11 STL */ Cell(); /* default constructor required by C++11 STL */
@@ -107,8 +108,14 @@ namespace Terminal {
bool operator!=( const Cell &x ) const { return !operator==( x ); } bool operator!=( const Cell &x ) const { return !operator==( x ); }
/* Accessors for contents field */
std::string debug_contents( void ) const; std::string debug_contents( void ) const;
bool empty( void ) const { return contents.empty(); }
/* 32 seems like a reasonable limit on combining characters */
bool full( void ) const { return contents.size() >= 32; }
void clear( void ) { contents.clear(); }
bool is_blank( void ) const bool is_blank( void ) const
{ {
// XXX fix. // XXX fix.
@@ -176,6 +183,15 @@ namespace Terminal {
} }
output.append( contents ); output.append( contents );
} }
/* Other accessors */
const Renditions& get_renditions( void ) const { return renditions; }
Renditions& get_renditions( void ) { return renditions; }
void set_renditions( const Renditions& r ) { renditions = r; }
unsigned int get_width( void ) const { return width; }
void set_width( unsigned int w ) { width = w; }
bool get_fallback( void ) const { return fallback; }
void set_fallback( bool f ) { fallback = f; }
}; };
class Row { class Row {
@@ -299,7 +315,8 @@ namespace Terminal {
void set_foreground_color( int x ) { renditions.set_foreground_color( x ); } void set_foreground_color( int x ) { renditions.set_foreground_color( x ); }
void set_background_color( int x ) { renditions.set_background_color( x ); } void set_background_color( int x ) { renditions.set_background_color( x ); }
void add_rendition( color_type x ) { renditions.set_rendition( x ); } void add_rendition( color_type x ) { renditions.set_rendition( x ); }
Renditions get_renditions( void ) const { return renditions; } const Renditions& get_renditions( void ) const { return renditions; }
Renditions& get_renditions( void ) { return renditions; }
int get_background_rendition( void ) const { return renditions.background_color; } int get_background_rendition( void ) const { return renditions.background_color; }
void save_cursor( void ); void save_cursor( void );
+1 -1
View File
@@ -148,7 +148,7 @@ static void Esc_DECALN( Framebuffer *fb, Dispatcher *dispatch __attribute((unuse
for ( int y = 0; y < fb->ds.get_height(); y++ ) { for ( int y = 0; y < fb->ds.get_height(); y++ ) {
for ( int x = 0; x < fb->ds.get_width(); x++ ) { for ( int x = 0; x < fb->ds.get_width(); x++ ) {
fb->reset_cell( fb->get_mutable_cell( y, x ) ); fb->reset_cell( fb->get_mutable_cell( y, x ) );
fb->get_mutable_cell( y, x )->contents.push_back( L'E' ); fb->get_mutable_cell( y, x )->append( 'E' );
} }
} }
} }
+1
View File
@@ -21,6 +21,7 @@ displaytests = \
emulation-cursor-motion.test \ emulation-cursor-motion.test \
emulation-multiline-scroll.test \ emulation-multiline-scroll.test \
emulation-wrap-across-frames.test \ emulation-wrap-across-frames.test \
prediction-unicode.test \
pty-deadlock.test \ pty-deadlock.test \
server-network-timeout.test \ server-network-timeout.test \
server-signal-timeout.test \ server-signal-timeout.test \
+5
View File
@@ -90,6 +90,11 @@ state. Alternately, this could use expect or something similar.
between tmux and mosh. It's expected to interact with its wrapped between tmux and mosh. It's expected to interact with its wrapped
command line as `expect` might do. This is not actually tested yet. command line as `expect` might do. This is not actually tested yet.
### Flags
`mosh-args`, `client-args` and `server-args` inject extra arguments
into the invocations of the respective commands.
## Logging and error reporting ## Logging and error reporting
Each execution action is run, and recorded in Each execution action is run, and recorded in
+9 -3
View File
@@ -96,7 +96,7 @@ mosh_client()
test_error "mosh_client: variables missing\n" test_error "mosh_client: variables missing\n"
fi fi
exec 2> "${MOSH_E2E_TEST}.client.stderr" exec 2> "${MOSH_E2E_TEST}.client.stderr"
exec "$MOSH_CLIENT" "$@" exec "$MOSH_CLIENT" $MOSH_CLIENT_ARGS "$@"
} }
mosh_server() mosh_server()
@@ -105,7 +105,7 @@ mosh_server()
test_error "mosh_server: variables missing\n" test_error "mosh_server: variables missing\n"
fi fi
exec 2> "${MOSH_E2E_TEST}.server.stderr" exec 2> "${MOSH_E2E_TEST}.server.stderr"
exec "$MOSH_SERVER" new -v -@ "$@" exec "$MOSH_SERVER" new -v $MOSH_SERVER_ARGS -@ "$@"
} }
# main # main
@@ -176,6 +176,12 @@ for i in $test_args; do
client=1;; client=1;;
post) post)
post=1;; post=1;;
mosh-args)
mosh_args=$(${test_script} mosh-args);;
client-args)
export MOSH_CLIENT_ARGS=$(${test_script} client-args);;
server-args)
export MOSH_SERVER_ARGS=$(${test_script} server-args);;
*) *)
error "unknown test type argument %s", $i error "unknown test type argument %s", $i
exit 99 exit 99
@@ -201,7 +207,7 @@ for run in $server_tests; do
export MOSH_SERVER="$PWD/../frontend/mosh-server" export MOSH_SERVER="$PWD/../frontend/mosh-server"
export MOSH_E2E_TEST="$PWD/${test_dir}/${run}" export MOSH_E2E_TEST="$PWD/${test_dir}/${run}"
# XXX need to quote special chars in server pathname here somehow # XXX need to quote special chars in server pathname here somehow
sut="../../scripts/mosh --client=${srcdir}/mosh-client --server=${srcdir}/mosh-server --local --bind-server=127.0.0.1 127.0.0.1" sut="../../scripts/mosh --client=${srcdir}/mosh-client --server=${srcdir}/mosh-server --local --bind-server=127.0.0.1 ${mosh_args} 127.0.0.1"
testarg=$run testarg=$run
if [ "$run" = "direct" ]; then if [ "$run" = "direct" ]; then
sut="" sut=""
+150
View File
@@ -0,0 +1,150 @@
#!/bin/sh
#
# This is a regression test for a bug seen where mosh-client's
# prediction code would sometimes show "gück" when "glück" was typed.
# mosh-client would output a predicted Unicode input character
# first as an 8-bit character containing the lowest 8 bits of the
# Unicode code point, then redraw it correctly with its UTF-8 sequence
# when the prediction is validated. For many accented Latin
# characters, the 8-bit character is an illegal UTF-8 code sequence.
# Most terminal emulators will output the Unicode replacement
# character, which is only visible until validation. urxvt, however,
# draws no character and does not change the cursor location on an
# illegal UTF-8 sequence, causing this bug to be visible as ongoing
# display corruption. A subset of wide characters (including CJK)
# will show display corruption with all terminal emulators, because a
# narrow replacement character will be drawn when a wide character
# should have been.
#
# tmux draws a replacement character for invalid UTF-8, and we
# depend on that in this test.
#
# Another similar failing case is typing "faĩl". In this case the "ĩ"
# would be predicted as ")" before being replaced by the
# correct character.
#
fail()
{
printf "$@" 2>&1
exit 99
}
PATH=$PATH:.:$srcdir
# Top-level wrapper.
if [ $# -eq 0 ]; then
e2e-test $0 tmux baseline mosh-args post
exit
fi
sleepf()
{
(sleep .1 || sleep 1) > /dev/null 2>&1
}
seq()
{
if [ $# -lt 1 -o $# -gt 3 ]; then
echo "bad args" >&2
fi
first=$1
incr=1
last=0
case $# in
3)
incr=$2
last=$3
;;
2)
last=$2
;;
1)
;;
esac
while :; do
printf '%d\n' $first
first=$(expr $first + $incr)
if [ $first -gt $last ]; then
break
fi
done
}
chr()
{
printf "\\$(printf %03o $1)"
}
utf8cp()
{
local c=$1
if [ $c -gt $((0x10ffff)) ]; then
fail "illegal Unicode code point %x\n" $c
elif [ $c -lt $((0x80)) ]; then
chr $c
elif [ $c -lt $((0x800)) ]; then
chr $(( (($c >> 6) & 0x1f) | 0xc0 ))
chr $(( ($c & 0x3f) | 0x80 ))
elif [ $c -lt $((0x10000)) ]; then
chr $(( (($c >> 12) & 0x0f) | 0xe0 ))
chr $(( (($c >> 6) & 0x3f) | 0x80 ))
chr $(( ($c & 0x3f) | 0x80 ))
elif [ $c -lt $((0x200000)) ]; then
chr $(( (($c >> 18) & 0x03) | 0xf0 ))
chr $(( (($c >> 12) & 0x3f) | 0x80 ))
chr $(( (($c >> 6) & 0x3f) | 0x80 ))
chr $(( ($c & 0x3f) | 0x80 ))
fi
}
tmux_commands()
{
for x in $(seq 1 5); do
for y in $(seq 1 5); do
for i in g l ü c k " " f a ĩ l " "; do
printf "send-keys '%s'\n" "$i"
sleepf
done
done
printf "send-keys 0x0d\n"
done
printf "send-keys 0x04\n"
sleep 5
}
tmux_stdin()
{
tmux_commands | "$@"
exit
}
baseline()
{
# Just receive and toss input in canonical mode.
cat > /dev/null
}
post()
{
# Look for bad output: ')' or \374
if [ -n "$(env -u LC_ALL -u LC_CTYPE -u LANGUAGE LANG=C egrep "%output %0 (\)|$(printf \\374))" $(basename $0).d/baseline.tmux.log)" ]; then
exit 1
fi
exit 0
}
case $1 in
tmux)
shift;
tmux_stdin "$@";;
baseline)
baseline;;
mosh-args)
printf "%s\n" "--predict=always";;
post)
post;;
*)
fail "unknown test argument %s\n" $1;;
esac