Skip to content

Commit d267252

Browse files
committed
Ticket #2325, #4480: Improve entering a directory with special characters
Handle trailing '\n' character in the directory name. Make sure to construct the cd command in physical lines no longer than 250 bytes so that we don't hit the small limit of the kernel's cooked mode tty buffer size on some platforms. tcsh still has problems entering directories with special characters (including invalid UTF-8) in their name. Other shells are now believed to handle any directory name properly. Signed-off-by: Egmont Koblinger <egmont@gmail.com>
1 parent a148b9a commit d267252

File tree

1 file changed

+141
-38
lines changed

1 file changed

+141
-38
lines changed

src/subshell/common.c

Lines changed: 141 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -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 */
@@ -1310,68 +1314,163 @@ init_subshell_precmd (void)
13101314

13111315
/* --------------------------------------------------------------------------------------------- */
13121316
/**
1313-
* Carefully quote directory name to allow entering any directory safely,
1314-
* no matter what weird characters it may contain in its name.
1315-
* NOTE: Treat directory name an untrusted data, don't allow it to cause
1316-
* executing any commands in the shell. Escape all control characters.
1317-
* Use following technique:
1318-
*
1319-
* printf(1) with format string containing a single conversion specifier,
1320-
* "b", and an argument which contains a copy of the string passed to
1321-
* subshell_name_quote() with all characters, except digits and letters,
1322-
* replaced by the backslash-escape sequence \0nnn, where "nnn" is the
1323-
* numeric value of the character converted to octal number.
1317+
* Carefully construct a 'cd' command that allows entering any directory safely. Two things to
1318+
* watch out for:
13241319
*
1325-
* cd "`printf '%b' 'ABC\0nnnDEF\0nnnXYZ'`"
1320+
* Enter any directory safely, no matter what special bytes its name contains (special shell
1321+
* characters, control characters, non-printable characters, invalid UTF-8 etc.).
1322+
* NOTE: Treat directory name as untrusted data, don't allow it to cause executing any commands in
1323+
* the shell!
13261324
*
1327-
* N.B.: Use single quotes for conversion specifier to work around
1328-
* tcsh 6.20+ parser breakage, see ticket #3852 for the details.
1325+
* Keep physical lines under COOKED_MODE_BUFFER_SIZE bytes, as in some kernels the buffer for the
1326+
* tty line's cooked mode is quite small. If the directory name is longer, we have to somehow
1327+
* construct a multiline cd command.
13291328
*/
13301329

13311330
static GString *
1332-
subshell_name_quote (const char *s)
1331+
create_cd_command (const char *s)
13331332
{
13341333
GString *ret;
13351334
const char *n;
1336-
const char *quote_cmd_start, *quote_cmd_end;
1335+
const char *quote_cmd_start, *before_wrap, *after_wrap, *quote_cmd_end;
1336+
const char *escape_fmt;
1337+
int line_length;
1338+
char buf[BUF_TINY];
13371339

1338-
if (mc_global.shell->type == SHELL_FISH)
1340+
if (mc_global.shell->type == SHELL_BASH || mc_global.shell->type == SHELL_ZSH)
1341+
{
1342+
/*
1343+
* bash and zsh: Use $'...\ooo...' notation (ooo is three octal digits).
1344+
*
1345+
* Use octal because hex mode likes to go multibyte.
1346+
*
1347+
* Line wrapping (if necessary) with a trailing backslash outside of quotes.
1348+
*/
1349+
quote_cmd_start = " cd $'";
1350+
before_wrap = "'\\";
1351+
after_wrap = "$'";
1352+
quote_cmd_end = "'";
1353+
escape_fmt = "\\%03o";
1354+
}
1355+
else if (mc_global.shell->type == SHELL_FISH)
13391356
{
1340-
quote_cmd_start = "(printf '%b' '";
1341-
quote_cmd_end = "')";
1357+
/*
1358+
* fish: Use ...\xHH... notation (HH is two hex digits).
1359+
*
1360+
* Its syntax requires that escapes go outside of quotes, and the rest is also safe there.
1361+
* Use hex because it only supports octal for low (up to octal 177 / decimal 127) bytes.
1362+
*
1363+
* Line wrapping (if necessary) with a trailing backslash.
1364+
*/
1365+
quote_cmd_start = " cd ";
1366+
before_wrap = "\\";
1367+
after_wrap = "";
1368+
quote_cmd_end = "";
1369+
escape_fmt = "\\x%02X";
1370+
}
1371+
else if (mc_global.shell->type == SHELL_TCSH)
1372+
{
1373+
/*
1374+
* tcsh: Use $'...\ooo...' notation (ooo is three octal digits).
1375+
*
1376+
* It doesn't support string contants spanning across multipline lines (a trailing
1377+
* backslash introduces a space), therefore construct the string in a tmp variable.
1378+
* Nevertheless emit a trailing backslash so it's just one line in its history.
1379+
*
1380+
* The :q modifier is needed to preserve newlines and other special chars.
1381+
*
1382+
* Note that tcsh's variables aren't binary clean, in its UTF-8 mode they are enforced
1383+
* to be valid UTF-8. So unfortunately we cannot enter every weird directory.
1384+
*/
1385+
quote_cmd_start = " set _mc_newdir=$'";
1386+
before_wrap = "'; \\";
1387+
after_wrap = " set _mc_newdir=${_mc_newdir:q}$'";
1388+
quote_cmd_end = "'; cd ${_mc_newdir:q}";
1389+
escape_fmt = "\\%03o";
13421390
}
13431391
else
13441392
{
1345-
quote_cmd_start = "\"`printf '%b' '";
1346-
quote_cmd_end = "'`\"";
1393+
/*
1394+
* Fallback / POSIX sh: Construct a command like this:
1395+
*
1396+
* _mc_newdir_=`printf '%b_' 'ABC\0oooDEF\0oooXYZ'` # ooo are three octal digits
1397+
* cd "${_mc_newdir_%_}"
1398+
*
1399+
* Command substitution removes final '\n's, hence the added and later removed '_' (#2325).
1400+
*
1401+
* Wrapping to new line with a trailing backslash outside of the innermost single quotes.
1402+
*/
1403+
quote_cmd_start = " _mc_newdir_=`printf '%b_' '";
1404+
before_wrap = "'\\";
1405+
after_wrap = "'";
1406+
quote_cmd_end = "'`; cd \"${_mc_newdir_%_}\"";
1407+
escape_fmt = "\\0%03o";
13471408
}
13481409

1410+
const int quote_cmd_start_len = strlen (quote_cmd_start);
1411+
const int before_wrap_len = strlen (before_wrap);
1412+
const int after_wrap_len = strlen (after_wrap);
1413+
const int quote_cmd_end_len = strlen (quote_cmd_end);
1414+
1415+
/* Measure the length of an escaped byte. In the unlikely case that it won't be uniform in some
1416+
* future shell, have an upper estimate by measuring the largest byte. */
1417+
const int escaped_char_len = sprintf (buf, escape_fmt, 0xFF);
1418+
13491419
ret = g_string_sized_new (64);
13501420

1421+
// Copy the beginning of the command to the buffer
1422+
g_string_append_len (ret, quote_cmd_start, quote_cmd_start_len);
1423+
13511424
// Prevent interpreting leading '-' as a switch for 'cd'
13521425
if (s[0] == '-')
13531426
g_string_append (ret, "./");
13541427

1355-
// Copy the beginning of the command to the buffer
1356-
g_string_append (ret, quote_cmd_start);
1428+
/* Sending physical lines over a certain small limit causes problems on some platforms,
1429+
* see ticket #4480. Make sure to wrap in time. See how large we can grow so that an
1430+
* additional line wrapping or closing string still fits. */
1431+
const int max_length = COOKED_MODE_BUFFER_SIZE - MAX (before_wrap_len, quote_cmd_end_len);
1432+
g_assert (max_length >= 64); // Make sure we have enough room to breathe.
13571433

1358-
/*
1359-
* Print every character except digits and letters as a backslash-escape
1360-
* sequence of the form \0nnn, where "nnn" is the numeric value of the
1361-
* character converted to octal number.
1362-
*/
1434+
line_length = ret->len;
1435+
1436+
/* Print (possibly multibyte) alphanumeric characters and '/' verbatim.
1437+
* Print everything else escaping each byte individually. */
13631438
for (const char *su = s; su[0] != '\0'; su = n)
13641439
{
13651440
n = str_cget_next_char_safe (su);
13661441

1367-
if (str_isalnum (su))
1442+
if (su[0] == '/' || str_isalnum (su))
1443+
{
1444+
if (line_length + (n - su) > max_length)
1445+
{
1446+
// wrap to next physical line
1447+
g_string_append_len (ret, before_wrap, before_wrap_len);
1448+
g_string_append_c (ret, '\n');
1449+
g_string_append_len (ret, after_wrap, after_wrap_len);
1450+
line_length = after_wrap_len;
1451+
}
1452+
// append character
13681453
g_string_append_len (ret, su, (size_t) (n - su));
1454+
line_length += (n - su);
1455+
}
13691456
else
13701457
for (size_t c = 0; c < (size_t) (n - su); c++)
1371-
g_string_append_printf (ret, "\\0%03o", (unsigned char) su[c]);
1458+
{
1459+
if (line_length + escaped_char_len > max_length)
1460+
{
1461+
// wrap to next physical line
1462+
g_string_append_len (ret, before_wrap, before_wrap_len);
1463+
g_string_append_c (ret, '\n');
1464+
g_string_append_len (ret, after_wrap, after_wrap_len);
1465+
line_length = after_wrap_len;
1466+
}
1467+
// append escaped byte
1468+
g_string_append_printf (ret, escape_fmt, (unsigned char) su[c]);
1469+
line_length += escaped_char_len;
1470+
}
13721471
}
13731472

1374-
g_string_append (ret, quote_cmd_end);
1473+
g_string_append_len (ret, quote_cmd_end, quote_cmd_end_len);
13751474

13761475
return ret;
13771476
}
@@ -1452,23 +1551,27 @@ do_subshell_chdir (const vfs_path_t *vpath, gboolean update_prompt)
14521551
feed_subshell (QUIETLY, TRUE);
14531552
}
14541553

1455-
/* The initial space keeps this out of the command history (in bash
1456-
because we set "HISTCONTROL=ignorespace") */
1457-
write_all (mc_global.tty.subshell_pty, " cd ", 4);
1458-
14591554
if (vpath == NULL)
1460-
write_all (mc_global.tty.subshell_pty, "/", 1);
1555+
{
1556+
/* The initial space keeps this out of the command history (in bash
1557+
because we set "HISTCONTROL=ignorespace") */
1558+
const char *cmd = " cd /";
1559+
write_all (mc_global.tty.subshell_pty, cmd, sizeof (cmd) - 1);
1560+
}
14611561
else
14621562
{
14631563
const char *translate = vfs_translate_path (vfs_path_as_str (vpath));
14641564

14651565
if (translate == NULL)
1466-
write_all (mc_global.tty.subshell_pty, ".", 1);
1566+
{
1567+
const char *cmd = " cd .";
1568+
write_all (mc_global.tty.subshell_pty, cmd, sizeof (cmd) - 1);
1569+
}
14671570
else
14681571
{
14691572
GString *temp;
14701573

1471-
temp = subshell_name_quote (translate);
1574+
temp = create_cd_command (translate);
14721575
write_all (mc_global.tty.subshell_pty, temp->str, temp->len);
14731576
g_string_free (temp, TRUE);
14741577
}

0 commit comments

Comments
 (0)