@@ -152,6 +152,10 @@ gboolean should_read_new_subshell_prompt;
152152/* Length of the buffer for all I/O with the subshell */
153153#define PTY_BUFFER_SIZE BUF_MEDIUM // Arbitrary; but keep it >= 80
154154
155+ /* Assume that the kernel's cooked mode buffer size might not be larger than this.
156+ * On Solaris it's 256 bytes, see ticket #4480. Shave off a few bytes, just in case. */
157+ #define COOKED_MODE_BUFFER_SIZE 250
158+
155159/*** file scope type declarations ****************************************************************/
156160
157161/* For pipes */
@@ -1304,68 +1308,162 @@ init_subshell_precmd (void)
13041308
13051309/* --------------------------------------------------------------------------------------------- */
13061310/**
1307- * Carefully quote directory name to allow entering any directory safely,
1308- * no matter what weird characters it may contain in its name.
1309- * NOTE: Treat directory name an untrusted data, don't allow it to cause
1310- * executing any commands in the shell. Escape all control characters.
1311- * Use following technique:
1312- *
1313- * printf(1) with format string containing a single conversion specifier,
1314- * "b", and an argument which contains a copy of the string passed to
1315- * subshell_name_quote() with all characters, except digits and letters,
1316- * replaced by the backslash-escape sequence \0nnn, where "nnn" is the
1317- * numeric value of the character converted to octal number.
1311+ * Carefully construct a 'cd' command that allows entering any directory safely. Two things to
1312+ * watch out for:
13181313 *
1319- * cd "`printf '%b' 'ABC\0nnnDEF\0nnnXYZ'`"
1314+ * Enter any directory safely, no matter what special bytes its name contains (special shell
1315+ * characters, control characters, non-printable characters, invalid UTF-8 etc.).
1316+ * NOTE: Treat directory name as untrusted data, don't allow it to cause executing any commands in
1317+ * the shell!
13201318 *
1321- * N.B.: Use single quotes for conversion specifier to work around
1322- * tcsh 6.20+ parser breakage, see ticket #3852 for the details.
1319+ * Keep physical lines under COOKED_MODE_BUFFER_SIZE bytes, as in some kernels the buffer for the
1320+ * tty line's cooked mode is quite small. If the directory name is longer, we have to somehow
1321+ * construct a multiline cd command.
13231322 */
13241323
13251324static GString *
1326- subshell_name_quote (const char * s )
1325+ create_cd_command (const char * s )
13271326{
13281327 GString * ret ;
13291328 const char * n ;
1330- const char * quote_cmd_start , * quote_cmd_end ;
1329+ const char * quote_cmd_start , * before_wrap , * after_wrap , * quote_cmd_end ;
1330+ const char * escape_fmt ;
1331+ int line_length ;
1332+ char buf [BUF_TINY ];
13311333
1332- if (mc_global .shell -> type == SHELL_FISH )
1334+ if (mc_global .shell -> type == SHELL_BASH || mc_global .shell -> type == SHELL_ZSH )
1335+ {
1336+ /*
1337+ * bash and zsh: Use $'...\ooo...' notation (ooo is three octal digits).
1338+ *
1339+ * Use octal because hex mode likes to go multibyte.
1340+ *
1341+ * Line wrapping (if necessary) with a trailing backslash outside of quotes.
1342+ */
1343+ quote_cmd_start = " cd $'" ;
1344+ before_wrap = "'\\" ;
1345+ after_wrap = "$'" ;
1346+ quote_cmd_end = "'" ;
1347+ escape_fmt = "\\%03o" ;
1348+ }
1349+ else if (mc_global .shell -> type == SHELL_FISH )
13331350 {
1334- quote_cmd_start = "(printf '%b' '" ;
1335- quote_cmd_end = "')" ;
1351+ /*
1352+ * fish: Use ...\xHH... notation (HH is two hex digits).
1353+ *
1354+ * Its syntax requires that escapes go outside of quotes, and the rest is also safe there.
1355+ * Use hex because it only supports octal for low (up to octal 177 / decimal 127) bytes.
1356+ *
1357+ * Line wrapping (if necessary) with a trailing backslash.
1358+ */
1359+ quote_cmd_start = " cd " ;
1360+ before_wrap = "\\" ;
1361+ after_wrap = "" ;
1362+ quote_cmd_end = "" ;
1363+ escape_fmt = "\\x%02X" ;
1364+ }
1365+ else if (mc_global .shell -> type == SHELL_TCSH )
1366+ {
1367+ /*
1368+ * tcsh: Use $'...\ooo...' notation (ooo is three octal digits).
1369+ *
1370+ * It doesn't support string contants spanning across multipline lines (a trailing
1371+ * backslash introduces a space), therefore construct the string in a tmp variable.
1372+ * Nevertheless emit a trailing backslash so it's just one line in its history.
1373+ *
1374+ * The :q modifier is needed to preserve newlines and other special chars.
1375+ *
1376+ * Note that tcsh's variables aren't binary clean, in its UTF-8 mode they are enforced
1377+ * to be valid UTF-8. So unfortunately we cannot enter every weird directory.
1378+ */
1379+ quote_cmd_start = " set _mc_newdir=$'" ;
1380+ before_wrap = "'; \\" ;
1381+ after_wrap = " set _mc_newdir=${_mc_newdir:q}$'" ;
1382+ quote_cmd_end = "'; cd ${_mc_newdir:q}" ;
1383+ escape_fmt = "\\%03o" ;
13361384 }
13371385 else
13381386 {
1339- quote_cmd_start = "\"`printf '%b' '" ;
1340- quote_cmd_end = "'`\"" ;
1387+ /*
1388+ * Fallback / POSIX sh: Construct a command like this:
1389+ *
1390+ * _mc_newdir_=`printf '%b_' 'ABC\0oooDEF\0oooXYZ'` # ooo are three octal digits
1391+ * cd "${_mc_newdir_%_}"
1392+ *
1393+ * Command substitution removes final '\n's, hence the added and later removed '_' (#2325).
1394+ *
1395+ * Wrapping to new line with a trailing backslash outside of the innermost single quotes.
1396+ */
1397+ quote_cmd_start = " _mc_newdir_=`printf '%b_' '" ;
1398+ before_wrap = "'\\" ;
1399+ after_wrap = "'" ;
1400+ quote_cmd_end = "'`; cd \"${_mc_newdir_%_}\"" ;
1401+ escape_fmt = "\\0%03o" ;
13411402 }
13421403
1404+ const int quote_cmd_start_len = strlen (quote_cmd_start );
1405+ const int before_wrap_len = strlen (before_wrap );
1406+ const int after_wrap_len = strlen (after_wrap );
1407+ const int quote_cmd_end_len = strlen (quote_cmd_end );
1408+
1409+ /* Measure the length of an escaped byte. In the unlikely case that it won't be uniform in some
1410+ * future shell, have an upper estimate by measuring the largest byte. */
1411+ const int escaped_char_len = sprintf (buf , escape_fmt , 0xFF );
1412+
13431413 ret = g_string_sized_new (64 );
13441414
1415+ // Copy the beginning of the command to the buffer
1416+ g_string_append_len (ret , quote_cmd_start , quote_cmd_start_len );
1417+
13451418 // Prevent interpreting leading '-' as a switch for 'cd'
13461419 if (s [0 ] == '-' )
13471420 g_string_append (ret , "./" );
13481421
1349- // Copy the beginning of the command to the buffer
1350- g_string_append (ret , quote_cmd_start );
1422+ /* Sending physical lines over a certain small limit causes problems on some platforms,
1423+ * see ticket #4480. Make sure to wrap in time. See how large we can grow so that an
1424+ * additional line wrapping or closing string still fits. */
1425+ const int max_length = COOKED_MODE_BUFFER_SIZE - MAX (before_wrap_len , quote_cmd_end_len );
1426+ g_assert (max_length >= 64 ); // make sure we have enough room to breathe
13511427
1352- /*
1353- * Print every character except digits and letters as a backslash-escape
1354- * sequence of the form \0nnn, where "nnn" is the numeric value of the
1355- * character converted to octal number.
1356- */
1428+ line_length = ret -> len ;
1429+
1430+ /* Print every character except digits and letters as a backslash-escape sequence. */
13571431 for (const char * su = s ; su [0 ] != '\0' ; su = n )
13581432 {
13591433 n = str_cget_next_char_safe (su );
13601434
13611435 if (str_isalnum (su ))
1436+ {
1437+ if (line_length + (n - su ) > max_length )
1438+ {
1439+ // wrap to next physical line
1440+ g_string_append_len (ret , before_wrap , before_wrap_len );
1441+ g_string_append_c (ret , '\n' );
1442+ g_string_append_len (ret , after_wrap , after_wrap_len );
1443+ line_length = after_wrap_len ;
1444+ }
1445+ // append character
13621446 g_string_append_len (ret , su , (size_t ) (n - su ));
1447+ line_length += (n - su );
1448+ }
13631449 else
13641450 for (size_t c = 0 ; c < (size_t ) (n - su ); c ++ )
1365- g_string_append_printf (ret , "\\0%03o" , (unsigned char ) su [c ]);
1451+ {
1452+ if (line_length + escaped_char_len > max_length )
1453+ {
1454+ // wrap to next physical line
1455+ g_string_append_len (ret , before_wrap , before_wrap_len );
1456+ g_string_append_c (ret , '\n' );
1457+ g_string_append_len (ret , after_wrap , after_wrap_len );
1458+ line_length = after_wrap_len ;
1459+ }
1460+ // append escaped byte
1461+ g_string_append_printf (ret , escape_fmt , (unsigned char ) su [c ]);
1462+ line_length += escaped_char_len ;
1463+ }
13661464 }
13671465
1368- g_string_append (ret , quote_cmd_end );
1466+ g_string_append_len (ret , quote_cmd_end , quote_cmd_end_len );
13691467
13701468 return ret ;
13711469}
@@ -1446,23 +1544,27 @@ do_subshell_chdir (const vfs_path_t *vpath, gboolean update_prompt)
14461544 feed_subshell (QUIETLY , TRUE);
14471545 }
14481546
1449- /* The initial space keeps this out of the command history (in bash
1450- because we set "HISTCONTROL=ignorespace") */
1451- write_all (mc_global .tty .subshell_pty , " cd " , 4 );
1452-
14531547 if (vpath == NULL )
1454- write_all (mc_global .tty .subshell_pty , "/" , 1 );
1548+ {
1549+ /* The initial space keeps this out of the command history (in bash
1550+ because we set "HISTCONTROL=ignorespace") */
1551+ const char * cmd = " cd /" ;
1552+ write_all (mc_global .tty .subshell_pty , cmd , sizeof (cmd ) - 1 );
1553+ }
14551554 else
14561555 {
14571556 const char * translate = vfs_translate_path (vfs_path_as_str (vpath ));
14581557
14591558 if (translate == NULL )
1460- write_all (mc_global .tty .subshell_pty , "." , 1 );
1559+ {
1560+ const char * cmd = " cd ." ;
1561+ write_all (mc_global .tty .subshell_pty , cmd , sizeof (cmd ) - 1 );
1562+ }
14611563 else
14621564 {
14631565 GString * temp ;
14641566
1465- temp = subshell_name_quote (translate );
1567+ temp = create_cd_command (translate );
14661568 write_all (mc_global .tty .subshell_pty , temp -> str , temp -> len );
14671569 g_string_free (temp , TRUE);
14681570 }
0 commit comments