* Extract operator + target from a `file_redirect` node. The target must be * a static word or string.
( node: Node, innerCommands: SimpleCommand[], varScope: Map<string, string>, )
| 1069 | * a static word or string. |
| 1070 | */ |
| 1071 | function walkFileRedirect( |
| 1072 | node: Node, |
| 1073 | innerCommands: SimpleCommand[], |
| 1074 | varScope: Map<string, string>, |
| 1075 | ): Redirect | ParseForSecurityResult { |
| 1076 | let op: Redirect['op'] | null = null |
| 1077 | let target: string | null = null |
| 1078 | let fd: number | undefined |
| 1079 | |
| 1080 | for (const child of node.children) { |
| 1081 | if (!child) continue |
| 1082 | if (child.type === 'file_descriptor') { |
| 1083 | fd = Number(child.text) |
| 1084 | } else if (child.type in REDIRECT_OPS) { |
| 1085 | op = REDIRECT_OPS[child.type] ?? null |
| 1086 | } else if (child.type === 'word' || child.type === 'number') { |
| 1087 | // SECURITY: `number` nodes can contain expansion children via the |
| 1088 | // `NN#<expansion>` arithmetic-base grammar quirk — same issue as |
| 1089 | // walkArgument's number case. `> 10#$(cmd)` runs cmd at runtime. |
| 1090 | // Plain word/number nodes have zero children. |
| 1091 | if (child.children.length > 0) return tooComplex(child) |
| 1092 | // Symmetry with walkArgument (~608): `echo foo > {a,b}` is an |
| 1093 | // ambiguous redirect in bash. tree-sitter actually emits a |
| 1094 | // `concatenation` node for brace targets (caught by the default |
| 1095 | // branch below), but check `word` text too for defense-in-depth. |
| 1096 | if (BRACE_EXPANSION_RE.test(child.text)) return tooComplex(child) |
| 1097 | // Unescape backslash sequences — same as walkArgument. Bash quote |
| 1098 | // removal turns `\X` → `X`. Without this, `cat < /proc/self/\environ` |
| 1099 | // stores target `/proc/self/\environ` which evades PROC_ENVIRON_RE, |
| 1100 | // but bash reads /proc/self/environ. |
| 1101 | target = child.text.replace(/\\(.)/g, '$1') |
| 1102 | } else if (child.type === 'raw_string') { |
| 1103 | target = stripRawString(child.text) |
| 1104 | } else if (child.type === 'string') { |
| 1105 | const s = walkString(child, innerCommands, varScope) |
| 1106 | if (typeof s !== 'string') return s |
| 1107 | target = s |
| 1108 | } else if (child.type === 'concatenation') { |
| 1109 | // `echo > "foo"bar` — tree-sitter produces a concatenation of string + |
| 1110 | // word children. walkArgument already validates concatenation (rejects |
| 1111 | // expansions, checks brace syntax) and returns the joined text. |
| 1112 | const s = walkArgument(child, innerCommands, varScope) |
| 1113 | if (typeof s !== 'string') return s |
| 1114 | target = s |
| 1115 | } else { |
| 1116 | return tooComplex(child) |
| 1117 | } |
| 1118 | } |
| 1119 | |
| 1120 | if (!op || target === null) { |
| 1121 | return { |
| 1122 | kind: 'too-complex', |
| 1123 | reason: 'Unrecognized redirect shape', |
| 1124 | nodeType: node.type, |
| 1125 | } |
| 1126 | } |
| 1127 | return { op, target, fd } |
| 1128 | } |
no test coverage detected