summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDenys Vlasenko <vda.linux@googlemail.com>2025-08-13 00:47:25 +0200
committerDenys Vlasenko <vda.linux@googlemail.com>2025-08-13 00:51:44 +0200
commit1847fee2d4ee7ce13edd676d3e69b8e11c3d1e7d (patch)
tree1ccd6426fe2a1e44b08f83d8da3a05059e6be265
parentf161bc628fc58ae9708e8499c93da657998d49f5 (diff)
hush: fix a corner case in "case" stmt, ctx_dsemicolon is in fact unused
function old new delta parse_stream 2446 2476 +30 done_word 797 800 +3 ------------------------------------------------------------------------------ (add/remove: 0/0 grow/shrink: 2/0 up/down: 33/0) Total: 33 bytes Signed-off-by: Denys Vlasenko <vda.linux@googlemail.com>
-rw-r--r--shell/hush.c91
-rw-r--r--shell/hush_test/hush-misc/case2.right1
-rwxr-xr-xshell/hush_test/hush-misc/case2.tests6
-rw-r--r--shell/hush_test/hush-misc/case3.right2
-rwxr-xr-xshell/hush_test/hush-misc/case3.tests8
5 files changed, 55 insertions, 53 deletions
diff --git a/shell/hush.c b/shell/hush.c
index b32d903ae..9ec813df2 100644
--- a/shell/hush.c
+++ b/shell/hush.c
@@ -748,9 +748,6 @@ struct parse_context {
smallint is_assignment; /* 0:maybe, 1:yes, 2:no, 3:keyword */
#if HAS_KEYWORDS
smallint ctx_res_w;
-#if ENABLE_HUSH_CASE
- smallint ctx_dsemicolon; /* ";;" seen */
-#endif
/* bitmask of FLAG_xxx, for figuring out valid reserved words */
int old_flag;
/* group we are enclosed in:
@@ -4170,7 +4167,7 @@ static const struct reserved_combo* reserved_word(struct parse_context *ctx)
{
# if ENABLE_HUSH_CASE
static const struct reserved_combo reserved_match = {
- "", RES_MATCH, NOT_ASSIGNMENT , FLAG_MATCH | FLAG_ESAC
+ "", RES_MATCH, NOT_ASSIGNMENT, FLAG_MATCH | FLAG_ESAC
};
# endif
const struct reserved_combo *r;
@@ -4266,7 +4263,7 @@ static const struct reserved_combo* reserved_word(struct parse_context *ctx)
* Look at it and update current command:
* update current command's argv/cmd_type/etc, fill in redirect name and type,
* check reserved-ness and assignment-ness, etc...
- * Normal return is 0. Syntax errors return 1.
+ * Normal return is 0. Syntax errors print error message and return 1.
* Note: on return, word is reset, but not o_free'd!
*/
static int done_word(struct parse_context *ctx)
@@ -4324,15 +4321,16 @@ static int done_word(struct parse_context *ctx)
#if HAS_KEYWORDS
# if ENABLE_HUSH_CASE
-
- if (ctx->ctx_dsemicolon
+ if (ctx->ctx_res_w == RES_MATCH
&& (ctx->word.has_quoted_part
|| strcmp(ctx->word.data, "esac") != 0
)
- ) { /* ";; WORD" but not "... PATTERN) CMD;; esac" */
- /* already done when ctx_dsemicolon was set to 1: */
- /* ctx->ctx_res_w = RES_MATCH; */
- ctx->ctx_dsemicolon = 0;
+ ) { /* ";; WORD" but not ";; esac" */
+ /* Do not match WORD as keyword:
+ * the WORD is a case match, can be keyword-like:
+ * if) echo got_if;;
+ * is allowed.
+ */
} else
# endif
# if defined(CMD_TEST2_SINGLEWORD_NOGLOB)
@@ -4345,13 +4343,13 @@ static int done_word(struct parse_context *ctx)
} else
# endif
if (!command->argv /* if it's the first word of command... */
- && !command->redirects /* no redirects yet... disallows: </dev/null ! true; echo $? */
+ && !command->redirects /* no redirects yet... disallows: </dev/null if true; then... */
# if ENABLE_HUSH_LOOPS
&& ctx->ctx_res_w != RES_FOR /* not after FOR or IN */
&& ctx->ctx_res_w != RES_IN
# endif
# if ENABLE_HUSH_CASE
- && ctx->ctx_res_w != RES_CASE /* not after CASE */
+ && ctx->ctx_res_w != RES_CASE /* not after CASE */
# endif
) {
const struct reserved_combo *reserved;
@@ -5648,7 +5646,7 @@ static struct pipe *parse_stream(char **pstring,
if (ch == EOF) {
/* Testcase: eval 'echo Ok\' */
/* bash-4.3.43 was removing backslash,
- * but 4.4.19 retains it, most other shells too
+ * but 4.4.19 retains it, most other shells retain too
*/
break;
}
@@ -5749,9 +5747,8 @@ static struct pipe *parse_stream(char **pstring,
}
/* ch == last eaten whitespace char */
#endif
- if (done_word(&ctx)) {
+ if (done_word(&ctx))
goto parse_error_exitcode1;
- }
if (ch == '\n') {
/* Is this a case when newline is simply ignored?
* Some examples:
@@ -5784,10 +5781,10 @@ static struct pipe *parse_stream(char **pstring,
if (pi->num_cmds != 0 /* check #1 */
&& pi->followup != PIPE_BG /* check #2 */
) {
- continue;
+ continue; /* ignore newline */
}
}
- /* Treat newline as a command separator. */
+ /* Treat newline as a command separator */
done_pipe(&ctx, PIPE_SEQ);
debug_printf_heredoc("heredoc_cnt:%d\n", heredoc_cnt);
if (heredoc_cnt) {
@@ -5805,11 +5802,11 @@ static struct pipe *parse_stream(char **pstring,
/* "cmd}" or "cmd }..." without semicolon or &:
* } is an ordinary char in this case, even inside { cmd; }
- * Pathological example: { ""}; } should exec "}" cmd
+ * Pathological example: { ""}; } should run "}" command.
*/
if (ch == '}') {
- if (ctx.word.length != 0 /* word} */
- || ctx.word.has_quoted_part /* ""} */
+ if (ctx.word.length != 0 /* word} */
+ || ctx.word.has_quoted_part /* ""} */
) {
goto ordinary_char;
}
@@ -5842,9 +5839,8 @@ static struct pipe *parse_stream(char **pstring,
)
#endif
) {
- if (done_word(&ctx)) {
+ if (done_word(&ctx))
goto parse_error_exitcode1;
- }
if (done_pipe(&ctx, PIPE_SEQ)) {
/* Testcase: sh -c 'date|;not_reached' */
syntax_error_unterm_ch('|');
@@ -5889,9 +5885,8 @@ static struct pipe *parse_stream(char **pstring,
switch (ch) {
case '>':
redir_fd = redirect_opt_num(&ctx.word);
- if (done_word(&ctx)) {
+ if (done_word(&ctx))
goto parse_error_exitcode1;
- }
redir_style = REDIRECT_OVERWRITE;
if (next == '>') {
redir_style = REDIRECT_APPEND;
@@ -5909,9 +5904,8 @@ static struct pipe *parse_stream(char **pstring,
continue; /* get next char */
case '<':
redir_fd = redirect_opt_num(&ctx.word);
- if (done_word(&ctx)) {
+ if (done_word(&ctx))
goto parse_error_exitcode1;
- }
redir_style = REDIRECT_INPUT;
if (next == '<') {
redir_style = REDIRECT_HEREDOC;
@@ -5951,7 +5945,7 @@ static struct pipe *parse_stream(char **pstring,
}
ch = i_getch(input);
if (ch == EOF)
- break;
+ goto eof;
}
continue; /* get next char */
}
@@ -6026,27 +6020,17 @@ static struct pipe *parse_stream(char **pstring,
}
#endif
case ';':
-#if ENABLE_HUSH_CASE
- case_semi:
-#endif
- if (done_word(&ctx)) {
+ if (done_word(&ctx))
goto parse_error_exitcode1;
- }
done_pipe(&ctx, PIPE_SEQ);
#if ENABLE_HUSH_CASE
- /* Eat multiple semicolons, detect
- * whether it means something special */
- while (1) {
- ch = i_peek_and_eat_bkslash_nl(input);
- if (ch != ';')
- break;
+ if (ctx.ctx_res_w == RES_CASE_BODY
+ && !is_blank /* is it really actual semicolon? */
+ && i_peek_and_eat_bkslash_nl(input) == ';' /* and next char is ';' too? */
+ ) {
ch = i_getch(input);
nommu_addchr(&ctx.as_string, ch);
- if (ctx.ctx_res_w == RES_CASE_BODY) {
- ctx.ctx_dsemicolon = 1;
- ctx.ctx_res_w = RES_MATCH;
- break;
- }
+ ctx.ctx_res_w = RES_MATCH; /* "we are in PATTERN)" */
}
#endif
new_cmd:
@@ -6056,9 +6040,8 @@ static struct pipe *parse_stream(char **pstring,
debug_printf_parse("ctx.is_assignment='%s'\n", assignment_flag[ctx.is_assignment]);
continue; /* get next char */
case '&':
- if (done_word(&ctx)) {
+ if (done_word(&ctx))
goto parse_error_exitcode1;
- }
if (ctx.pipe->num_cmds == 0 && IS_NULL_CMD(ctx.command)) {
/* Testcase: sh -c '&& date' */
/* Testcase: sh -c '&' */
@@ -6082,9 +6065,8 @@ static struct pipe *parse_stream(char **pstring,
}
goto new_cmd;
case '|':
- if (done_word(&ctx)) {
+ if (done_word(&ctx))
goto parse_error_exitcode1;
- }
#if ENABLE_HUSH_CASE
if (ctx.ctx_res_w == RES_MATCH)
break; /* we are in case's "word | word)" */
@@ -6135,8 +6117,12 @@ static struct pipe *parse_stream(char **pstring,
}
case ')':
#if ENABLE_HUSH_CASE
- if (ctx.ctx_res_w == RES_MATCH)
- goto case_semi;
+ if (ctx.ctx_res_w == RES_MATCH) {
+ if (done_word(&ctx))
+ goto parse_error_exitcode1;
+ done_pipe(&ctx, PIPE_SEQ);
+ goto new_cmd;
+ }
#endif
case '}':
/* proper use of this character is caught by end_trigger:
@@ -6150,7 +6136,7 @@ static struct pipe *parse_stream(char **pstring,
bb_error_msg_and_die("BUG: unexpected %c", ch);
}
} /* while (1) */
-
+ eof:
/* Reached EOF */
if (heredoc_cnt) {
syntax_error_unterm_str("here document");
@@ -6165,9 +6151,8 @@ static struct pipe *parse_stream(char **pstring,
goto parse_error_exitcode1;
}
- if (done_word(&ctx)) {
+ if (done_word(&ctx))
goto parse_error_exitcode1;
- }
o_free_and_set_NULL(&ctx.word);
if (done_pipe(&ctx, PIPE_SEQ)) {
/* Testcase: sh -c 'date |' */
diff --git a/shell/hush_test/hush-misc/case2.right b/shell/hush_test/hush-misc/case2.right
new file mode 100644
index 000000000..4780199de
--- /dev/null
+++ b/shell/hush_test/hush-misc/case2.right
@@ -0,0 +1 @@
+hush: syntax error: unexpected )
diff --git a/shell/hush_test/hush-misc/case2.tests b/shell/hush_test/hush-misc/case2.tests
new file mode 100755
index 000000000..b9475392e
--- /dev/null
+++ b/shell/hush_test/hush-misc/case2.tests
@@ -0,0 +1,6 @@
+# had a parser bug which incorrectly detected ";;"
+case w in
+a);
+b);;
+esac
+echo Should not be reached
diff --git a/shell/hush_test/hush-misc/case3.right b/shell/hush_test/hush-misc/case3.right
new file mode 100644
index 000000000..02a29ccbc
--- /dev/null
+++ b/shell/hush_test/hush-misc/case3.right
@@ -0,0 +1,2 @@
+Keyword-like word may be in pattern
+Ok:0
diff --git a/shell/hush_test/hush-misc/case3.tests b/shell/hush_test/hush-misc/case3.tests
new file mode 100755
index 000000000..63b9dfd99
--- /dev/null
+++ b/shell/hush_test/hush-misc/case3.tests
@@ -0,0 +1,8 @@
+if=if
+case if in
+if) echo "Keyword-like word may be in pattern";;
+fi) echo "Not reached";;
+# esac) echo "Not reached";; # not accepted by bash 5.2.15
+do) echo "Not reached"
+esac
+echo Ok:$?