265 lines
7.6 KiB
Dart
265 lines
7.6 KiB
Dart
// Dart port of splitCommand_DEPRECATED from old_repo/utils/bash/commands.ts.
|
|
//
|
|
// Splits a bash compound command into individual subcommands by tokenizing
|
|
// the input the same way a shell-quote parser would — respecting single/double
|
|
// quotes, backslash escapes, and the standard control operators. Redirections
|
|
// (>, >>, >&, 2>&1, etc.) are stripped so they don't appear as separate
|
|
// "commands" in the permission-prompt list.
|
|
|
|
const _controlOperators = {
|
|
'&&', '||', ';', ';;', '|',
|
|
};
|
|
|
|
// Control + redirection operators that are filtered out when building the final
|
|
// subcommand list.
|
|
const _allSupportedOperators = {
|
|
'&&', '||', ';', ';;', '|', '>&', '>', '>>',
|
|
};
|
|
|
|
const _allowedFileDescriptors = {'0', '1', '2'};
|
|
|
|
/// Checks whether a redirect target is a plain static path (no shell expansion).
|
|
bool _isStaticRedirectTarget(String target) {
|
|
if (target.contains(RegExp(r"[\s'" + '"' + "]"))) return false;
|
|
if (target.isEmpty) return false;
|
|
if (target.startsWith('#')) return false;
|
|
return !target.startsWith('!') &&
|
|
!target.startsWith('=') &&
|
|
!target.contains('\$') &&
|
|
!target.contains('`') &&
|
|
!target.contains('*') &&
|
|
!target.contains('?') &&
|
|
!target.contains('[') &&
|
|
!target.contains('{') &&
|
|
!target.contains('~') &&
|
|
!target.contains('(') &&
|
|
!target.contains('<') &&
|
|
!target.startsWith('&');
|
|
}
|
|
|
|
/// Tokenizes a bash command string into a flat list of token strings and
|
|
/// operator strings. Handles single-quoted, double-quoted, and
|
|
/// backslash-escaped input. Newlines outside quotes are treated as ;
|
|
/// separators (same as bash IFS behaviour in non-interactive mode).
|
|
///
|
|
/// Returns each token or operator as a separate list element.
|
|
List<String> _tokenize(String command) {
|
|
final tokens = <String>[];
|
|
final buf = StringBuffer();
|
|
int i = 0;
|
|
final len = command.length;
|
|
|
|
void flush() {
|
|
final s = buf.toString();
|
|
if (s.isNotEmpty) {
|
|
tokens.add(s);
|
|
buf.clear();
|
|
}
|
|
}
|
|
|
|
while (i < len) {
|
|
final c = command[i];
|
|
|
|
if (c == '\\' && i + 1 < len) {
|
|
// backslash-newline: line continuation — skip both chars
|
|
if (command[i + 1] == '\n') {
|
|
i += 2;
|
|
continue;
|
|
}
|
|
// otherwise backslash-escape: absorb both chars into current token
|
|
buf.write(c);
|
|
buf.write(command[i + 1]);
|
|
i += 2;
|
|
continue;
|
|
}
|
|
|
|
if (c == "'") {
|
|
// single-quoted: everything literal until next '
|
|
buf.write(c);
|
|
i++;
|
|
while (i < len && command[i] != "'") {
|
|
buf.write(command[i]);
|
|
i++;
|
|
}
|
|
if (i < len) {
|
|
buf.write(command[i]); // closing '
|
|
i++;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (c == '"') {
|
|
// double-quoted: allow \x escapes but nothing else is special
|
|
buf.write(c);
|
|
i++;
|
|
while (i < len && command[i] != '"') {
|
|
if (command[i] == '\\' && i + 1 < len) {
|
|
buf.write(command[i]);
|
|
buf.write(command[i + 1]);
|
|
i += 2;
|
|
} else {
|
|
buf.write(command[i]);
|
|
i++;
|
|
}
|
|
}
|
|
if (i < len) {
|
|
buf.write(command[i]); // closing "
|
|
i++;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Newline outside quotes acts as a command separator
|
|
if (c == '\n') {
|
|
flush();
|
|
tokens.add(';'); // treat as ;
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
// Whitespace separates tokens
|
|
if (c == ' ' || c == '\t') {
|
|
flush();
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
// check for two-char operators first: &&, ||, >>, >&, ;;
|
|
if (i + 1 < len) {
|
|
final two = command.substring(i, i + 2);
|
|
if (two == '&&' || two == '||' || two == '>>' || two == '>&' || two == ';;') {
|
|
flush();
|
|
tokens.add(two);
|
|
i += 2;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// single-char operators: ;, |, >
|
|
if (c == ';' || c == '|' || c == '>') {
|
|
flush();
|
|
tokens.add(c);
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
// Comment — skip to end of line
|
|
if (c == '#' && buf.isEmpty) {
|
|
while (i < len && command[i] != '\n') {
|
|
i++;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
buf.write(c);
|
|
i++;
|
|
}
|
|
|
|
flush();
|
|
return tokens;
|
|
}
|
|
|
|
/// Splits a compound bash command into individual subcommands by tokenizing
|
|
/// and then stripping redirections and filtering out control operators.
|
|
///
|
|
/// This is the Dart equivalent of splitCommand_DEPRECATED in commands.ts.
|
|
List<String> splitCommand(String command) {
|
|
final tokens = _tokenize(command);
|
|
|
|
// Strip output redirections (>, >>, >&, 2>&1 etc.)
|
|
// Same logic as the JS implementation — mark tokens for removal.
|
|
final parts = List<String?>.from(tokens);
|
|
|
|
for (int i = 0; i < parts.length; i++) {
|
|
final part = parts[i];
|
|
if (part == null) continue;
|
|
|
|
if (part == '>&' || part == '>' || part == '>>') {
|
|
final prevPart = i > 0 ? parts[i - 1]?.trimRight() : null;
|
|
final nextPart = i + 1 < parts.length ? parts[i + 1]?.trim() : null;
|
|
final afterNext = i + 2 < parts.length ? parts[i + 2]?.trim() : null;
|
|
|
|
if (nextPart == null) continue;
|
|
|
|
bool shouldStrip = false;
|
|
bool stripThird = false;
|
|
|
|
if (part == '>&' && _allowedFileDescriptors.contains(nextPart)) {
|
|
// 2>&1 style
|
|
shouldStrip = true;
|
|
} else if (part == '>' &&
|
|
nextPart == '&' &&
|
|
afterNext != null &&
|
|
_allowedFileDescriptors.contains(afterNext)) {
|
|
shouldStrip = true;
|
|
stripThird = true;
|
|
} else if (part == '>' &&
|
|
nextPart.startsWith('&') &&
|
|
nextPart.length > 1 &&
|
|
_allowedFileDescriptors.contains(nextPart.substring(1))) {
|
|
shouldStrip = true;
|
|
} else if ((part == '>' || part == '>>') && _isStaticRedirectTarget(nextPart)) {
|
|
shouldStrip = true;
|
|
}
|
|
|
|
if (shouldStrip) {
|
|
// strip trailing file descriptor from previous token (e.g., '2' from 'echo foo 2')
|
|
if (prevPart != null &&
|
|
prevPart.length >= 3 &&
|
|
_allowedFileDescriptors.contains(prevPart[prevPart.length - 1]) &&
|
|
prevPart[prevPart.length - 2] == ' ') {
|
|
parts[i - 1] = prevPart.substring(0, prevPart.length - 2);
|
|
}
|
|
|
|
parts[i] = null;
|
|
parts[i + 1] = null;
|
|
if (stripThird && i + 2 < parts.length) {
|
|
parts[i + 2] = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Filter nulls and empty strings, then filter control operators
|
|
return parts
|
|
.where((p) => p != null && p.isNotEmpty)
|
|
.cast<String>()
|
|
.where((p) => !_controlOperators.contains(p))
|
|
.where((p) => !_allSupportedOperators.contains(p))
|
|
.toList();
|
|
}
|
|
|
|
/// Returns true when the command contains a cd subcommand (used by the
|
|
/// cd + git security check in pathValidation).
|
|
bool commandHasAnyCd(String command) {
|
|
final subs = splitCommand(command);
|
|
return subs.any((s) {
|
|
final trimmed = s.trim();
|
|
return trimmed == 'cd' ||
|
|
trimmed.startsWith('cd ') ||
|
|
trimmed.startsWith('cd\t');
|
|
});
|
|
}
|
|
|
|
/// Returns true when the given subcommand (already split) is a plain cd invocation.
|
|
bool isNormalizedCdCommand(String command) {
|
|
final trimmed = command.trim();
|
|
return trimmed == 'cd' ||
|
|
trimmed.startsWith('cd ') ||
|
|
trimmed.startsWith('cd\t');
|
|
}
|
|
|
|
/// Returns true when the given subcommand is a git invocation
|
|
/// (with optional leading safe env var assignments stripped off).
|
|
bool isNormalizedGitCommand(String command) {
|
|
// strip safe env vars at the front — simple heuristic
|
|
var trimmed = command.trim();
|
|
while (true) {
|
|
final m = RegExp(r'^[A-Za-z_]\w*=[^ \t]* ').firstMatch(trimmed);
|
|
if (m == null) break;
|
|
trimmed = trimmed.substring(m.end);
|
|
}
|
|
return trimmed == 'git' ||
|
|
trimmed.startsWith('git ') ||
|
|
trimmed.startsWith('git\t');
|
|
}
|