Skip to content

Commit ceeb70e

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 platfors. 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 93483d5 commit ceeb70e

File tree

1 file changed

+134
-35
lines changed

1 file changed

+134
-35
lines changed

src/subshell/common.c

Lines changed: 134 additions & 35 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 */
@@ -1306,42 +1310,103 @@ init_subshell_precmd (void)
13061310

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

13271326
static GString *
1328-
subshell_name_quote (const char *s)
1327+
create_cd_command (const char *s)
13291328
{
13301329
GString *ret;
13311330
const char *n;
1332-
const char *quote_cmd_start, *quote_cmd_end;
1331+
const char *quote_cmd_start, *before_wrap, *after_wrap, *quote_cmd_end;
1332+
const char *escape_fmt;
1333+
int line_length;
1334+
char buf[BUF_TINY];
13331335

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

1406+
/* Measure the length of an escaped byte. In the unlikely case that it won't be uniform in some
1407+
* future shell, have an upper estimate by measuring the largest byte. */
1408+
const int escaped_char_len = sprintf (buf, escape_fmt, 0xFF);
1409+
13451410
ret = g_string_sized_new (64);
13461411

13471412
// Prevent interpreting leading '-' as a switch for 'cd'
@@ -1351,20 +1416,50 @@ subshell_name_quote (const char *s)
13511416
// Copy the beginning of the command to the buffer
13521417
g_string_append (ret, quote_cmd_start);
13531418

1354-
/*
1355-
* Print every character except digits and letters as a backslash-escape
1356-
* sequence of the form \0nnn, where "nnn" is the numeric value of the
1357-
* character converted to octal number.
1358-
*/
1419+
/* Sending physical lines over a certain small limit causes problems on some platforms,
1420+
* see ticket #4480. Make sure to wrap in time. See how large we can grow so that an
1421+
* additional escaped byte and the closing string still fit. */
1422+
const int max_length =
1423+
COOKED_MODE_BUFFER_SIZE - MAX (strlen (before_wrap), strlen (quote_cmd_end));
1424+
g_assert (max_length >= 64); // Make sure we have enough room to breathe.
1425+
1426+
line_length = ret->len;
1427+
1428+
/* Print (possibly multibyte) alphanumeric characters and '/' verbatim.
1429+
* Print everything else escaping each byte individually. */
13591430
for (const char *su = s; su[0] != '\0'; su = n)
13601431
{
13611432
n = str_cget_next_char_safe (su);
13621433

1363-
if (str_isalnum (su))
1434+
if (su[0] == '/' || str_isalnum (su))
1435+
{
1436+
if (line_length + (n - su) > max_length)
1437+
{
1438+
// wrap to next physical line
1439+
g_string_append (ret, before_wrap);
1440+
g_string_append_c (ret, '\n');
1441+
g_string_append (ret, after_wrap);
1442+
line_length = strlen (after_wrap);
1443+
}
1444+
// append character
13641445
g_string_append_len (ret, su, (size_t) (n - su));
1446+
line_length += (n - su);
1447+
}
13651448
else
13661449
for (size_t c = 0; c < (size_t) (n - su); c++)
1367-
g_string_append_printf (ret, "\\0%03o", (unsigned char) su[c]);
1450+
{
1451+
if (line_length + escaped_char_len > max_length)
1452+
{
1453+
// wrap to next physical line
1454+
g_string_append (ret, before_wrap);
1455+
g_string_append_c (ret, '\n');
1456+
g_string_append (ret, after_wrap);
1457+
line_length = strlen (after_wrap);
1458+
}
1459+
// append escaped byte
1460+
g_string_append_printf (ret, escape_fmt, (unsigned char) su[c]);
1461+
line_length += escaped_char_len;
1462+
}
13681463
}
13691464

13701465
g_string_append (ret, quote_cmd_end);
@@ -1448,23 +1543,27 @@ do_subshell_chdir (const vfs_path_t *vpath, gboolean update_prompt)
14481543
feed_subshell (QUIETLY, TRUE);
14491544
}
14501545

1451-
/* The initial space keeps this out of the command history (in bash
1452-
because we set "HISTCONTROL=ignorespace") */
1453-
write_all (mc_global.tty.subshell_pty, " cd ", 4);
1454-
14551546
if (vpath == NULL)
1456-
write_all (mc_global.tty.subshell_pty, "/", 1);
1547+
{
1548+
/* The initial space keeps this out of the command history (in bash
1549+
because we set "HISTCONTROL=ignorespace") */
1550+
const char *cmd = " cd /";
1551+
write_all (mc_global.tty.subshell_pty, cmd, sizeof (cmd) - 1);
1552+
}
14571553
else
14581554
{
14591555
const char *translate = vfs_translate_path (vfs_path_as_str (vpath));
14601556

14611557
if (translate == NULL)
1462-
write_all (mc_global.tty.subshell_pty, ".", 1);
1558+
{
1559+
const char *cmd = " cd .";
1560+
write_all (mc_global.tty.subshell_pty, cmd, sizeof (cmd) - 1);
1561+
}
14631562
else
14641563
{
14651564
GString *temp;
14661565

1467-
temp = subshell_name_quote (translate);
1566+
temp = create_cd_command (translate);
14681567
write_all (mc_global.tty.subshell_pty, temp->str, temp->len);
14691568
g_string_free (temp, TRUE);
14701569
}

0 commit comments

Comments
 (0)