From cc0420b36b3b03de7dd75f44ef263adaf49b0e06 Mon Sep 17 00:00:00 2001 From: terrafrost Date: Sat, 23 May 2015 11:55:03 -0500 Subject: [PATCH 1/4] ANSI: improve vt100 terminal emulation store each coordinate's attributes independently and add support for a few more escape codes --- phpseclib/File/ANSI.php | 285 ++++++++++++++++++++++------------------ 1 file changed, 154 insertions(+), 131 deletions(-) diff --git a/phpseclib/File/ANSI.php b/phpseclib/File/ANSI.php index 3ff1b7d4..5e32ed64 100644 --- a/phpseclib/File/ANSI.php +++ b/phpseclib/File/ANSI.php @@ -117,6 +117,22 @@ class File_ANSI */ var $old_y; + /** + * An empty attribute cell + * + * @var Object + * @access private + */ + var $base_attr_cell; + + /** + * The current attribute cell + * + * @var Object + * @access private + */ + var $attr_cell; + /** * An empty attribute row * @@ -141,62 +157,6 @@ class File_ANSI */ var $attrs; - /** - * The current foreground color - * - * @var String - * @access private - */ - var $foreground; - - /** - * The current background color - * - * @var String - * @access private - */ - var $background; - - /** - * Bold flag - * - * @var Boolean - * @access private - */ - var $bold; - - /** - * Underline flag - * - * @var Boolean - * @access private - */ - var $underline; - - /** - * Blink flag - * - * @var Boolean - * @access private - */ - var $blink; - - /** - * Reverse flag - * - * @var Boolean - * @access private - */ - var $reverse; - - /** - * Color flag - * - * @var Boolean - * @access private - */ - var $color; - /** * Current ANSI code * @@ -205,6 +165,14 @@ class File_ANSI */ var $ansi; + /** + * Tokenization + * + * @var Array + * @access private + */ + var $tokenization; + /** * Default Constructor. * @@ -213,6 +181,16 @@ class File_ANSI */ function File_ANSI() { + $attr_cell = new stdClass(); + $attr_cell->bold = false; + $attr_cell->underline = false; + $attr_cell->blink = false; + $attr_cell->background = 'black'; + $attr_cell->foreground = 'white'; + $attr_cell->reverse = false; + $this->base_attr_cell = clone($attr_cell); + $this->attr_cell = clone($attr_cell); + $this->setHistory(200); $this->setDimensions(80, 24); } @@ -232,17 +210,9 @@ class File_ANSI $this->max_y = $y - 1; $this->x = $this->y = 0; $this->history = $this->history_attrs = array(); - $this->attr_row = array_fill(0, $this->max_x + 1, ''); + $this->attr_row = array_fill(0, $this->max_x + 2, $this->base_attr_cell); $this->screen = array_fill(0, $this->max_y + 1, ''); $this->attrs = array_fill(0, $this->max_y + 1, $this->attr_row); - $this->foreground = 'white'; - $this->background = 'black'; - $this->bold = false; - $this->underline = false; - $this->blink = false; - $this->reverse = false; - $this->color = false; - $this->ansi = ''; } @@ -278,6 +248,7 @@ class File_ANSI */ function appendString($source) { + $this->tokenization = array(''); for ($i = 0; $i < strlen($source); $i++) { if (strlen($this->ansi)) { $this->ansi.= $source[$i]; @@ -294,6 +265,8 @@ class File_ANSI default: continue 2; } + $this->tokenization[] = $this->ansi; + $this->tokenization[] = ''; // http://ascii-table.com/ansi-escape-sequences-vt-100.php switch ($this->ansi) { case "\x1B[H": // Move cursor to upper left corner @@ -315,7 +288,7 @@ class File_ANSI case "\x1B[K": // Clear screen from cursor right $this->screen[$this->y] = substr($this->screen[$this->y], 0, $this->x); - array_splice($this->attrs[$this->y], $this->x + 1); + array_splice($this->attrs[$this->y], $this->x + 1, $this->max_x - $this->x, array_fill($this->x, $this->max_x - $this->x - 1, $this->base_attr_cell)); break; case "\x1B[2K": // Clear entire line $this->screen[$this->y] = str_repeat(' ', $this->x); @@ -323,6 +296,7 @@ class File_ANSI break; case "\x1B[?1h": // set cursor key to application case "\x1B[?25h": // show the cursor + case "\x1B(B": // set united states g0 character set break; case "\x1BE": // Move to next line $this->_newLine(); @@ -330,6 +304,10 @@ class File_ANSI break; default: switch (true) { + case preg_match('#\x1B\[(\d+)B#', $this->ansi, $match): // Move cursor down n lines + $this->old_y = $this->y; + $this->y+= $match[1]; + break; case preg_match('#\x1B\[(\d+);(\d+)H#', $this->ansi, $match): // Move cursor to screen location v,h $this->old_x = $this->x; $this->old_y = $this->y; @@ -338,64 +316,42 @@ class File_ANSI break; case preg_match('#\x1B\[(\d+)C#', $this->ansi, $match): // Move cursor right n lines $this->old_x = $this->x; - $x = $match[1] - 1; + $this->x+= $match[1]; + break; + case preg_match('#\x1B\[(\d+)D#', $this->ansi, $match): // Move cursor left n lines + $this->old_x = $this->x; + $this->x-= $match[1]; break; case preg_match('#\x1B\[(\d+);(\d+)r#', $this->ansi, $match): // Set top and bottom lines of a window break; case preg_match('#\x1B\[(\d*(?:;\d*)*)m#', $this->ansi, $match): // character attributes + $attr_cell = &$this->attr_cell; $mods = explode(';', $match[1]); foreach ($mods as $mod) { switch ($mod) { case 0: // Turn off character attributes - $this->attrs[$this->y][$this->x] = ''; - - if ($this->bold) $this->attrs[$this->y][$this->x].= ''; - if ($this->underline) $this->attrs[$this->y][$this->x].= ''; - if ($this->blink) $this->attrs[$this->y][$this->x].= ''; - if ($this->color) $this->attrs[$this->y][$this->x].= ''; - - if ($this->reverse) { - $temp = $this->background; - $this->background = $this->foreground; - $this->foreground = $temp; - } - - $this->bold = $this->underline = $this->blink = $this->color = $this->reverse = false; + $attr_cell = clone($this->base_attr_cell); break; case 1: // Turn bold mode on - if (!$this->bold) { - $this->attrs[$this->y][$this->x] = ''; - $this->bold = true; - } + $attr_cell->bold = true; break; case 4: // Turn underline mode on - if (!$this->underline) { - $this->attrs[$this->y][$this->x] = ''; - $this->underline = true; - } + $attr_cell->underline = true; break; case 5: // Turn blinking mode on - if (!$this->blink) { - $this->attrs[$this->y][$this->x] = ''; - $this->blink = true; - } + $attr_cell->blink = true; break; case 7: // Turn reverse video on - $this->reverse = !$this->reverse; - $temp = $this->background; - $this->background = $this->foreground; - $this->foreground = $temp; - $this->attrs[$this->y][$this->x] = ''; - if ($this->color) { - $this->attrs[$this->y][$this->x] = '' . $this->attrs[$this->y][$this->x]; - } - $this->color = true; + $attr_cell->reverse = !$attr_cell->reverse; + $temp = $attr_cell->background; + $attr_cell->background = $attr_cell->foreground; + $attr_cell->foreground = $temp; break; default: // set colors - //$front = $this->reverse ? &$this->background : &$this->foreground; - $front = &$this->{ $this->reverse ? 'background' : 'foreground' }; - //$back = $this->reverse ? &$this->foreground : &$this->background; - $back = &$this->{ $this->reverse ? 'foreground' : 'background' }; + //$front = $attr_cell->reverse ? &$attr_cell->background : &$attr_cell->foreground; + $front = &$attr_cell->{ $attr_cell->reverse ? 'background' : 'foreground' }; + //$back = $attr_cell->reverse ? &$attr_cell->foreground : &$attr_cell->background; + $back = &$attr_cell->{ $attr_cell->reverse ? 'foreground' : 'background' }; switch ($mod) { case 30: $front = 'black'; break; case 31: $front = 'red'; break; @@ -416,28 +372,22 @@ class File_ANSI case 47: $back = 'white'; break; default: - user_error('Unsupported attribute: ' . $mod); + //user_error('Unsupported attribute: ' . $mod); $this->ansi = ''; break 2; } - - unset($temp); - $this->attrs[$this->y][$this->x] = ''; - if ($this->color) { - $this->attrs[$this->y][$this->x] = '' . $this->attrs[$this->y][$this->x]; - } - $this->color = true; } } break; default: - user_error("{$this->ansi} unsupported\r\n"); + //user_error("{$this->ansi} is unsupported\r\n"); } } $this->ansi = ''; continue; } + $this->tokenization[count($this->tokenization) - 1].= $source[$i]; switch ($source[$i]) { case "\r": $this->x = 0; @@ -445,12 +395,32 @@ class File_ANSI case "\n": $this->_newLine(); break; + case "\x08": // backspace + if ($this->x) { + $this->x--; + $this->attrs[$this->y][$this->x] = clone($this->base_attr_cell); + $this->screen[$this->y] = substr_replace( + $this->screen[$this->y], + $source[$i], + $this->x, + 1 + ); + } + break; case "\x0F": // shift break; case "\x1B": // start ANSI escape code + $this->tokenization[count($this->tokenization) - 1] = substr($this->tokenization[count($this->tokenization) - 1], 0, -1); + //if (!strlen($this->tokenization[count($this->tokenization) - 1])) { + // array_pop($this->tokenization); + //} $this->ansi.= "\x1B"; break; default: + $this->attrs[$this->y][$this->x] = clone($this->attr_cell); + if ($this->x > strlen($this->screen[$this->y])) { + $this->screen[$this->y] = str_repeat(' ', $this->x); + } $this->screen[$this->y] = substr_replace( $this->screen[$this->y], $source[$i], @@ -498,6 +468,63 @@ class File_ANSI $this->y++; } + /** + * Returns the current coordinate without preformating + * + * @access private + * @return String + */ + function _processCoordinate($last_attr, $cur_attr, $char) + { + $output = ''; + + if ($last_attr != $cur_attr) { + $close = $open = ''; + if ($last_attr->foreground != $cur_attr->foreground) { + if ($cur_attr->foreground != 'white') { + $open.= ''; + } + if ($last_attr->foreground != 'white') { + $close = '' . $close; + } + } + if ($last_attr->background != $cur_attr->background) { + if ($cur_attr->background != 'black') { + $open.= ''; + } + if ($last_attr->backtground != 'black') { + $close = '' . $close; + } + } + if ($last_attr->bold != $cur_attr->bold) { + if ($cur_attr->bold) { + $open.= ''; + } else { + $close = '' . $close; + } + } + if ($last_attr->underline != $cur_attr->underline) { + if ($cur_attr->underline) { + $open.= ''; + } else { + $close = '' . $close; + } + } + if ($last_attr->blink != $cur_attr->blink) { + if ($cur_attr->blink) { + $open.= ''; + } else { + $close = '' . $close; + } + } + $output.= $close . $open; + } + + $output.= htmlspecialchars($char); + + return $output; + } + /** * Returns the current screen without preformating * @@ -507,18 +534,16 @@ class File_ANSI function _getScreen() { $output = ''; + $last_attr = $this->base_attr_cell; for ($i = 0; $i <= $this->max_y; $i++) { - for ($j = 0; $j <= $this->max_x + 1; $j++) { - if (isset($this->attrs[$i][$j])) { - $output.= $this->attrs[$i][$j]; - } - if (isset($this->screen[$i][$j])) { - $output.= htmlspecialchars($this->screen[$i][$j]); - } + for ($j = 0; $j <= $this->max_x; $j++) { + $cur_attr = $this->attrs[$i][$j]; + $output.= $this->_processCoordinate($last_attr, $cur_attr, isset($this->screen[$i][$j]) ? $this->screen[$i][$j] : ''); + $last_attr = $this->attrs[$i][$j]; } $output.= "\r\n"; } - return rtrim($output); + return $output; //rtrim($output); } /** @@ -529,7 +554,7 @@ class File_ANSI */ function getScreen() { - return '
' . $this->_getScreen() . '
'; + return '
' . $this->_getScreen() . '
'; } /** @@ -541,19 +566,17 @@ class File_ANSI function getHistory() { $scrollback = ''; + $last_attr = $this->base_attr_cell; for ($i = 0; $i < count($this->history); $i++) { for ($j = 0; $j <= $this->max_x + 1; $j++) { - if (isset($this->history_attrs[$i][$j])) { - $scrollback.= $this->history_attrs[$i][$j]; - } - if (isset($this->history[$i][$j])) { - $scrollback.= htmlspecialchars($this->history[$i][$j]); - } + $cur_attr = $this->history_attrs[$i][$j]; + $scrollback.= $this->_processCoordinate($last_attr, $cur_attr, isset($this->history[$i][$j]) ? $this->history[$i][$j] : ''); + $last_attr = $this->history_attrs[$i][$j]; } $scrollback.= "\r\n"; } $scrollback.= $this->_getScreen(); - return '
' . $scrollback . '
'; + return '
' . $scrollback . '
'; } } From 2bb70a1c22fe5ee63e7b40aad0a6136dde211aae Mon Sep 17 00:00:00 2001 From: terrafrost Date: Sat, 23 May 2015 16:51:57 -0500 Subject: [PATCH 2/4] ANSI: rendering improvements --- phpseclib/File/ANSI.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/phpseclib/File/ANSI.php b/phpseclib/File/ANSI.php index 5e32ed64..e518f6e5 100644 --- a/phpseclib/File/ANSI.php +++ b/phpseclib/File/ANSI.php @@ -543,7 +543,10 @@ class File_ANSI } $output.= "\r\n"; } - return $output; //rtrim($output); + $output = substr($output, 0, -2); + // close any remaining open tags + $output.= $this->_processCoordinate($last_attr, $this->base_attr_cell, ''); + return rtrim($output); } /** @@ -575,7 +578,10 @@ class File_ANSI } $scrollback.= "\r\n"; } + $base_attr_cell = $this->base_attr_cell; + $this->base_attr_cell = $last_attr; $scrollback.= $this->_getScreen(); + $this->base_attr_cell = $base_attr_cell; return '
' . $scrollback . '
'; } From 48191bdeec4b8ecd70cbe0ae64b770b171362f7e Mon Sep 17 00:00:00 2001 From: terrafrost Date: Sat, 23 May 2015 17:39:17 -0500 Subject: [PATCH 3/4] ANSI: fix typo --- phpseclib/File/ANSI.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpseclib/File/ANSI.php b/phpseclib/File/ANSI.php index e518f6e5..c8496e1f 100644 --- a/phpseclib/File/ANSI.php +++ b/phpseclib/File/ANSI.php @@ -492,7 +492,7 @@ class File_ANSI if ($cur_attr->background != 'black') { $open.= ''; } - if ($last_attr->backtground != 'black') { + if ($last_attr->background != 'black') { $close = '' . $close; } } From 4f19085e68822d4a64c1a007640a4d0c5def3ca6 Mon Sep 17 00:00:00 2001 From: terrafrost Date: Sat, 23 May 2015 17:41:16 -0500 Subject: [PATCH 4/4] Tests: add ANSI unit test --- tests/Unit/File/ANSITest.php | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 tests/Unit/File/ANSITest.php diff --git a/tests/Unit/File/ANSITest.php b/tests/Unit/File/ANSITest.php new file mode 100644 index 00000000..dbb67af0 --- /dev/null +++ b/tests/Unit/File/ANSITest.php @@ -0,0 +1,31 @@ + + * @copyright 2014 Jim Wigginton + * @license http://www.opensource.org/licenses/mit-license.html MIT License + */ + +require_once 'File/ANSI.php'; + +class Unit_File_ANSITest extends PhpseclibTestCase +{ + public function testCase1() + { + $str = "\x1B[07m"; // turn reverse video on + $str.= "aaaaaaaaaaaaaaaaaa"; + $str.= "\x1B[10D"; // move cursor left 10 lines + $str.= "\x1B[m"; // reset everything + $str.= "bbb"; + + $ansi = new File_ANSI(); + $ansi->appendString($str); + + $expected = '
';
+        $expected.= 'aaaaaaaa';
+        $expected.= 'bbb';
+        $expected.= 'aaaaaaa';
+        $expected.= '
'; + + $this->assertSame($ansi->getScreen(), $expected); + } +}