Skip to content

Commit 43a7c79

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 <[email protected]>
1 parent 338a9e8 commit 43a7c79

File tree

1 file changed

+139
-37
lines changed

1 file changed

+139
-37
lines changed

src/subshell/common.c

Lines changed: 139 additions & 37 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 */
@@ -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

13251324
static 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

Comments
 (0)