1/*
2 * Copyright 2019, Haiku, Inc. All rights reserved.
3 * Distributed under the terms of the MIT License.
4 *
5 * Authors:
6 *		Augustin Cavalier <waddlesplash>
7 */
8
9/* Pass this tool a string, and it will parse it into an argv and execvp(). */
10
11
12#include <stdio.h>
13#include <stdlib.h>
14#include <errno.h>
15#include <unistd.h>
16
17
18static void
19append_char(char c, char** arg, int* argLen, int* argBufferLen)
20{
21	if ((*argLen + 1) >= *argBufferLen) {
22		*arg = realloc(*arg, *argBufferLen + 32);
23		if (*arg == NULL) {
24			puts("exec: oom");
25			exit(1);
26		}
27		*argBufferLen += 32;
28	}
29
30	(*arg)[*argLen] = c;
31	(*argLen)++;
32}
33
34
35static void
36parse_quoted(const char* str, int* pos, char** currentArg, int* currentArgLen,
37	int* currentArgBufferLen)
38{
39	char end = str[*pos];
40	while (1) {
41		char c;
42		(*pos)++;
43		c = str[*pos];
44		if (c == '\0') {
45			puts("exec: mismatched quotes");
46			exit(1);
47		}
48		if (c == end)
49			break;
50
51		switch (c) {
52		case '\\':
53			(*pos)++;
54			// fall through
55		default:
56			append_char(str[*pos], currentArg, currentArgLen,
57				currentArgBufferLen);
58		break;
59		}
60	}
61}
62
63
64int
65main(int argc, const char* argv[])
66{
67	char** args = NULL, *currentArg = NULL;
68	const char* str;
69	int argsLen = 0, argsBufferLen = 0, currentArgLen = 0,
70		currentArgBufferLen = 0, encounteredNewlineAt = -1,
71		modifiedEnvironment = 0, pos;
72
73	if (argc != 2) {
74		printf("usage: %s \"program arg 'arg1' ...\"\n", argv[0]);
75		return 1;
76	}
77
78	str = argv[1];
79	pos = 0;
80	while (1) {
81		switch (str[pos]) {
82		case '\r':
83		case '\n':
84			// In normal shells, this would imply a second command.
85			// We don't support that here, so we need to make sure
86			// that either we have not parsed any arguments yet,
87			// or there are no more arguments pushed after this.
88			if (argsLen == 0 && currentArgLen == 0 && !modifiedEnvironment)
89				break;
90			encounteredNewlineAt = argsLen + 1;
91			// fall through
92		case ' ':
93		case '\t':
94		case '\0':
95			if (currentArgLen == 0)
96				break; // do nothing
97			if (encounteredNewlineAt == argsLen) {
98				puts("exec: running multiple commands not supported!");
99				return 1;
100			}
101
102			// the current argument hasn't been terminated, do that now
103			append_char('\0', &currentArg, &currentArgLen,
104				&currentArgBufferLen);
105
106			// handle environs
107			{
108			char* val;
109			if (argsLen == 0 && (val = strstr(currentArg, "=")) != NULL) {
110				const char* dollar;
111				char* newVal = NULL;
112				*val = '\0';
113				val++;
114
115				// handle trivial variable substitution, i.e. VAL=$VAL:...
116				dollar = strstr(val, "$");
117				if (dollar != NULL) {
118					const char* oldVal;
119					int oldValLen, valLen, nameLen;
120
121					if (dollar != val) {
122						puts("exec: environ expansion not at start of "
123							"line unsupported");
124						return 1;
125					}
126					val++; // skip the $
127					valLen = strlen(val);
128					nameLen = strlen(currentArg);
129
130					// if the new value does not start with the environ name
131					// (which is broken by a non-alphanumeric character), bail.
132					if (strncmp(val, currentArg, nameLen) != 0
133							|| isalnum(val[nameLen])) {
134						puts("exec: environ expansion of other variables "
135							"unsupported");
136						return 1;
137					}
138
139					// get the old value and actually do the expansion
140					oldVal = getenv(currentArg);
141					oldValLen = strlen(oldVal);
142					newVal = malloc(valLen + oldValLen + 1);
143					memcpy(newVal, oldVal, oldValLen);
144					memcpy(newVal + oldValLen, val + nameLen, valLen + 1);
145					val = newVal;
146				}
147
148				setenv(currentArg, val, 1);
149				free(newVal);
150				modifiedEnvironment = 1;
151
152				free(currentArg);
153				currentArg = NULL;
154				currentArgLen = 0;
155				currentArgBufferLen = 0;
156				break;
157			}
158			}
159
160			// actually add the argument to the array
161			if ((argsLen + 2) >= argsBufferLen) {
162				args = realloc(args, (argsBufferLen + 8) * sizeof(char*));
163				if (args == NULL) {
164					puts("exec: oom");
165					return 1;
166				}
167				argsBufferLen += 8;
168			}
169
170			args[argsLen] = currentArg;
171			args[argsLen + 1] = NULL;
172			argsLen++;
173
174			currentArg = NULL;
175			currentArgLen = 0;
176			currentArgBufferLen = 0;
177		break;
178
179		case '\'':
180		case '"':
181			parse_quoted(str, &pos, &currentArg, &currentArgLen,
182				&currentArgBufferLen);
183		break;
184
185		case '\\':
186			pos++;
187			// don't append newlines to the current argument
188			if (str[pos] == '\r' || str[pos] == '\n')
189				break;
190			// fall through
191		default:
192			append_char(str[pos], &currentArg, &currentArgLen,
193				&currentArgBufferLen);
194		break;
195		}
196		if (str[pos] == '\0')
197			break;
198		pos++;
199	}
200
201	pos = execvp(args[0], args);
202	if (pos != 0)
203		printf("exec failed: %s\n", strerror(errno));
204	return pos;
205}
206