ohmu
·
2026-10-15
1#include "common/printing.h"
2#include "common/jsonconfig.h"
3#include "modules/command/command.h"
4#include "detection/command/command.h"
5
6#if defined(_WIN32)
7// `ffStrbufGetline()` splits on `\n` only, so a CRLF line keeps its trailing `\r`. That is what every
8// `cmd.exe` builtin and most native Windows tools produce, and the leftover carriage return makes the
9// terminal jump back to the start of the line. The `\r` is overwritten with a NUL, so `line` stays a
10// usable C string; the length without the `\r` is returned for callers that need to copy the line.
11//
12// `len` itself must stay the distance from the line start to the delimiter: `ffStrbufGetdelim()`
13// advances with `*lineptr += *n`, so shortening it would make the next call start inside this line.
14static size_t trimCarriageReturn(char* line, size_t len) {
15 if (len > 0 && line[len - 1] == '\r') {
16 line[len - 1] = '\0';
17 return len - 1;
18 }
19 return len;
20}
21#endif
22
23bool ffPrintCommand(FFCommandOptions* options) {
24 FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate();
25 const char* error = ffDetectCommand(options, &result);
26
27 if (error) {
28 ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Command), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
29 return false;
30 }
31
32 if (!result.length) {
33 ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Command), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "No result generated");
34 return false;
35 }
36
37 if (options->splitLines) {
38 uint32_t index = 0;
39 char* line = nullptr;
40 size_t len = 0;
41 while (ffStrbufGetline(&line, &len, &result)) {
42#if defined(_WIN32)
43 trimCarriageReturn(line, len);
44#endif
45 if (options->moduleArgs.outputFormat.length == 0) {
46 ffPrintLogoAndKey(FF_MODULE_GET_DISPLAY_NAME(Command), ++index, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
47 puts(line);
48 } else {
49 FF_PRINT_FORMAT_CHECKED(FF_MODULE_GET_DISPLAY_NAME(Command), ++index, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) { FF_ARG(line, "result") }));
50 }
51 }
52 } else {
53 if (options->moduleArgs.outputFormat.length == 0) {
54 ffPrintLogoAndKey(FF_MODULE_GET_DISPLAY_NAME(Command), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
55 ffStrbufPutTo(&result, stdout);
56 } else {
57 FF_PRINT_FORMAT_CHECKED(FF_MODULE_GET_DISPLAY_NAME(Command), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) { FF_ARG(result, "result") }));
58 }
59 }
60
61 return true;
62}
63
64void ffParseCommandJsonObject(FFCommandOptions* options, yyjson_val* module) {
65 yyjson_val *key, *val;
66 size_t idx, max;
67 yyjson_obj_foreach (module, idx, max, key, val) {
68 if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
69 continue;
70 }
71
72 if (unsafe_yyjson_equals_str(key, "shell")) {
73 ffStrbufSetJsonVal(&options->shell, val);
74 continue;
75 }
76
77 if (unsafe_yyjson_equals_str(key, "param")) {
78 ffStrbufSetJsonVal(&options->param, val);
79 continue;
80 }
81
82 if (unsafe_yyjson_equals_str(key, "text")) {
83 ffStrbufSetJsonVal(&options->text, val);
84 continue;
85 }
86
87 if (unsafe_yyjson_equals_str(key, "useStdErr")) {
88 options->useOutput = yyjson_get_bool(val) ? FF_PROCESS_OUTPUT_STDERR_BIT : FF_PROCESS_OUTPUT_STDOUT_BIT;
89 ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Command), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "useStdErr is deprecated, use useOutput instead");
90 continue;
91 }
92
93 if (unsafe_yyjson_equals_str(key, "useOutput")) {
94 int value;
95 const char* error = ffJsonConfigParseEnum(val, &value, (FFKeyValuePair[]) {
96 { "stdout", FF_PROCESS_OUTPUT_STDOUT_BIT },
97 { "stderr", FF_PROCESS_OUTPUT_STDERR_BIT },
98 { "both", FF_PROCESS_OUTPUT_BOTH },
99 {},
100 });
101 if (error) {
102 ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Command), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Invalid %s value: %s", unsafe_yyjson_get_str(key), error);
103 } else {
104 options->useOutput = (FFProcessOutputType) value;
105 }
106 continue;
107 }
108
109 if (unsafe_yyjson_equals_str(key, "parallel")) {
110 options->parallel = yyjson_get_bool(val);
111 continue;
112 }
113
114 if (unsafe_yyjson_equals_str(key, "splitLines")) {
115 options->splitLines = yyjson_get_bool(val);
116 continue;
117 }
118
119 ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Command), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
120 }
121}
122
123[[gnu::cold]]
124void ffGenerateCommandJsonConfig(FFCommandOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
125 ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
126
127 yyjson_mut_obj_add_strbuf(doc, module, "shell", &options->shell);
128 yyjson_mut_obj_add_strbuf(doc, module, "param", &options->param);
129 yyjson_mut_obj_add_strbuf(doc, module, "text", &options->text);
130 switch (options->useOutput) {
131 case FF_PROCESS_OUTPUT_STDOUT_BIT:
132 yyjson_mut_obj_add_str(doc, module, "useOutput", "stdout");
133 break;
134 case FF_PROCESS_OUTPUT_STDERR_BIT:
135 yyjson_mut_obj_add_str(doc, module, "useOutput", "stderr");
136 break;
137 case FF_PROCESS_OUTPUT_BOTH:
138 yyjson_mut_obj_add_str(doc, module, "useOutput", "both");
139 break;
140 default:
141 break;
142 }
143 yyjson_mut_obj_add_bool(doc, module, "parallel", options->parallel);
144 yyjson_mut_obj_add_bool(doc, module, "splitLines", options->splitLines);
145}
146
147bool ffGenerateCommandJsonResult([[maybe_unused]] FFCommandOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
148 FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate();
149 const char* error = ffDetectCommand(options, &result);
150
151 if (error) {
152 yyjson_mut_obj_add_str(doc, module, "error", error);
153 return false;
154 }
155
156 if (!result.length) {
157 yyjson_mut_obj_add_str(doc, module, "error", "No result generated");
158 return false;
159 }
160
161 if (options->splitLines) {
162 yyjson_mut_val* jsonArray = yyjson_mut_obj_add_arr(doc, module, "result");
163 char* line = nullptr;
164 size_t len = 0;
165 while (ffStrbufGetline(&line, &len, &result)) {
166 // `len` has to stay the distance to the delimiter (see `trimCarriageReturn`), so the
167 // possibly shorter length of the line itself is tracked separately.
168#if defined(_WIN32)
169 size_t contentLen = trimCarriageReturn(line, len);
170#else
171 size_t contentLen = len;
172#endif
173 yyjson_mut_arr_add_strncpy(doc, jsonArray, line, contentLen);
174 }
175 } else {
176 yyjson_mut_obj_add_strbuf(doc, module, "result", &result);
177 }
178
179 return true;
180}
181
182void ffInitCommandOptions(FFCommandOptions* options) {
183 ffOptionInitModuleArg(&options->moduleArgs, "");
184
185 ffStrbufInitStatic(&options->shell,
186#ifdef _WIN32
187 "cmd.exe"
188#else
189 "/bin/sh"
190#endif
191 );
192 ffStrbufInitStatic(&options->param,
193#ifdef _WIN32
194 "/d/c"
195#else
196 "-c"
197#endif
198 );
199 ffStrbufInit(&options->text);
200 // Capture both streams by default: most tools log to stdout or stderr without saying which, and
201 // the previous single-stream default silently dropped one of them.
202 options->useOutput = FF_PROCESS_OUTPUT_BOTH;
203 options->parallel = true;
204 options->splitLines = false;
205}
206
207void ffDestroyCommandOptions(FFCommandOptions* options) {
208 ffOptionDestroyModuleArg(&options->moduleArgs);
209 ffStrbufDestroy(&options->shell);
210 ffStrbufDestroy(&options->param);
211 ffStrbufDestroy(&options->text);
212}
213
214FFModuleBaseInfo ffCommandModuleInfo = {
215 .name = "Command",
216 .description = "Run custom shell scripts",
217 .displayName = {
218 .en = "Command",
219 .ar = "الأمر",
220 .cs = "Příkaz",
221 .de = "Befehl",
222 .es = "Comando",
223 .fi = "Komento",
224 .fr = "Commande",
225 .gl = "Comando",
226 .he = "פקודה",
227 .id = "Perintah",
228 .it = "Comando",
229 .ja = "コマンド",
230 .ko = "명령어",
231 .pl = "Polecenie",
232 .pt = "Comando",
233 .ru = "Команда",
234 .tr = "Komut",
235 .uk = "Команда",
236 .vi = "Lệnh",
237 .zh_CN = "命令",
238 .zh_TW = "命令",
239 },
240 .initOptions = (void*) ffInitCommandOptions,
241 .destroyOptions = (void*) ffDestroyCommandOptions,
242 .parseJsonObject = (void*) ffParseCommandJsonObject,
243 .printModule = (void*) ffPrintCommand,
244 .generateJsonResult = (void*) ffGenerateCommandJsonResult,
245 .generateJsonConfig = (void*) ffGenerateCommandJsonConfig,
246 .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
247 { "Command result", "result" },
248 })),
249};