Page MenuHomePhabricator

D12966.diff
No OneTemporary

D12966.diff

diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -322,11 +322,13 @@
'PhutilSyntaxHighlighterException' => 'markup/syntax/highlighter/PhutilSyntaxHighlighterException.php',
'PhutilSystem' => 'utils/PhutilSystem.php',
'PhutilSystemTestCase' => 'utils/__tests__/PhutilSystemTestCase.php',
+ 'PhutilTerminalString' => 'xsprintf/PhutilTerminalString.php',
'PhutilTestPhobject' => 'object/__tests__/PhutilTestPhobject.php',
'PhutilTortureTestDaemon' => 'daemon/torture/PhutilTortureTestDaemon.php',
'PhutilTranslation' => 'internationalization/PhutilTranslation.php',
'PhutilTranslator' => 'internationalization/PhutilTranslator.php',
'PhutilTranslatorTestCase' => 'internationalization/__tests__/PhutilTranslatorTestCase.php',
+ 'PhutilTsprintfTestCase' => 'xsprintf/__tests__/PhutilTsprintfTestCase.php',
'PhutilTwitchAuthAdapter' => 'auth/PhutilTwitchAuthAdapter.php',
'PhutilTwitchFuture' => 'future/twitch/PhutilTwitchFuture.php',
'PhutilTwitterAuthAdapter' => 'auth/PhutilTwitterAuthAdapter.php',
@@ -464,6 +466,7 @@
'queryfx' => 'xsprintf/queryfx.php',
'queryfx_all' => 'xsprintf/queryfx.php',
'queryfx_one' => 'xsprintf/queryfx.php',
+ 'tsprintf' => 'xsprintf/tsprintf.php',
'urisprintf' => 'xsprintf/urisprintf.php',
'vcsprintf' => 'xsprintf/csprintf.php',
'vjsprintf' => 'xsprintf/jsprintf.php',
@@ -478,6 +481,7 @@
'xsprintf_ldap' => 'xsprintf/ldapsprintf.php',
'xsprintf_mercurial' => 'xsprintf/hgsprintf.php',
'xsprintf_query' => 'xsprintf/qsprintf.php',
+ 'xsprintf_terminal' => 'xsprintf/tsprintf.php',
'xsprintf_uri' => 'xsprintf/urisprintf.php',
),
'xmap' => array(
@@ -720,10 +724,12 @@
'PhutilSyntaxHighlighterException' => 'Exception',
'PhutilSystem' => 'Phobject',
'PhutilSystemTestCase' => 'PhutilTestCase',
+ 'PhutilTerminalString' => 'Phobject',
'PhutilTestPhobject' => 'Phobject',
'PhutilTortureTestDaemon' => 'PhutilDaemon',
'PhutilTranslation' => 'Phobject',
'PhutilTranslatorTestCase' => 'PhutilTestCase',
+ 'PhutilTsprintfTestCase' => 'PhutilTestCase',
'PhutilTwitchAuthAdapter' => 'PhutilOAuthAuthAdapter',
'PhutilTwitchFuture' => 'FutureProxy',
'PhutilTwitterAuthAdapter' => 'PhutilOAuth1AuthAdapter',
diff --git a/src/xsprintf/PhutilTerminalString.php b/src/xsprintf/PhutilTerminalString.php
new file mode 100644
--- /dev/null
+++ b/src/xsprintf/PhutilTerminalString.php
@@ -0,0 +1,63 @@
+<?php
+
+/**
+ * String escaped for terminal output. See @{function:tsprintf}.
+ */
+final class PhutilTerminalString extends Phobject {
+
+ private $string;
+
+ public function __construct($string) {
+ $this->string = $string;
+ }
+
+ public function __toString() {
+ return $this->string;
+ }
+
+ public static function escapeStringValue($value, $allow_whitespace) {
+ if ($value instanceof PhutilTerminalString) {
+ return (string)$value;
+ }
+
+ $value = (string)$value;
+
+ static $escape_map;
+ if ($escape_map === null) {
+ $escape_map = array(
+ chr(0x00) => '<NUL>',
+ chr(0x07) => '<BEL>',
+ chr(0x08) => '<BS>',
+ chr(0x09) => '<TAB>',
+ chr(0x0A) => '<LF>',
+ chr(0x0D) => '<CR>',
+ chr(0x1B) => '<ESC>',
+ chr(0x7F) => '<DEL>',
+ );
+
+ for ($ii = 0; $ii < 32; $ii++) {
+ $c = chr($ii);
+ if (empty($escape_map[$c])) {
+ $escape_map[$c] = sprintf('<0x%02X>', $ii);
+ }
+ }
+ }
+
+ $map = $escape_map;
+ if ($allow_whitespace) {
+ unset($map["\r"]);
+ unset($map["\n"]);
+ unset($map["\t"]);
+ }
+
+ $value = str_replace(array_keys($map), array_values($map), $value);
+
+ // In this mode, we additionally escape any <CR> which is not immediately
+ // followed by <LF>.
+ if ($allow_whitespace) {
+ $value = preg_replace('/\r(?!\n)/', '<CR>', $value);
+ }
+
+ return $value;
+ }
+}
diff --git a/src/xsprintf/__tests__/PhutilTsprintfTestCase.php b/src/xsprintf/__tests__/PhutilTsprintfTestCase.php
new file mode 100644
--- /dev/null
+++ b/src/xsprintf/__tests__/PhutilTsprintfTestCase.php
@@ -0,0 +1,24 @@
+<?php
+
+final class PhutilTsprintfTestCase extends PhutilTestCase {
+
+ public function testTsprintf() {
+ $this->assertEqual(
+ '<NUL>',
+ (string)tsprintf('%s', "\0"));
+
+ $this->assertEqual(
+ '<ESC>[31mred<ESC>[39m',
+ (string)tsprintf('%s', "\x1B[31mred\x1B[39m"));
+
+ $block = "1\r\n2\r3\n4";
+
+ $this->assertEqual(
+ '1<CR><LF>2<CR>3<LF>4',
+ (string)tsprintf('%s', $block));
+ $this->assertEqual(
+ "1\r\n2<CR>3\n4",
+ (string)tsprintf('%B', $block));
+ }
+
+}
diff --git a/src/xsprintf/tsprintf.php b/src/xsprintf/tsprintf.php
new file mode 100644
--- /dev/null
+++ b/src/xsprintf/tsprintf.php
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * Format text for terminal output. This function behaves like `sprintf`,
+ * except that all the normal conversions (like "%s") will be properly escaped,
+ * and additional conversions are supported:
+ *
+ * %B (Block)
+ * Escapes text, but preserves tabs and newlines.
+ *
+ * %R (Raw String)
+ * Inserts raw, unescaped text. DANGEROUS!
+ *
+ * Particularly, this will escape terminal control characters.
+ */
+function tsprintf($pattern /* , ... */) {
+ $args = func_get_args();
+ $string = xsprintf('xsprintf_terminal', null, $args);
+ return new PhutilTerminalString($string);
+}
+
+/**
+ * Callback for terminal encoding, see @{function:tsprintf} for use.
+ */
+function xsprintf_terminal($userdata, &$pattern, &$pos, &$value, &$length) {
+ $type = $pattern[$pos];
+
+ switch ($type) {
+ case 's':
+ $value = PhutilTerminalString::escapeStringValue($value, false);
+ $type = 's';
+ break;
+ case 'B':
+ $value = PhutilTerminalString::escapeStringValue($value, true);
+ $type = 's';
+ break;
+ case 'R':
+ $type = 's';
+ break;
+ }
+
+ $pattern[$pos] = $type;
+}

File Metadata

Mime Type
text/plain
Expires
Mon, May 13, 6:01 AM (3 w, 2 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6292322
Default Alt Text
D12966.diff (5 KB)

Event Timeline