Serenity Operating System
1/*
2 * Copyright (c) 2020-2021, Sergey Bugaev <bugaevc@serenityos.org>
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include <AK/Assertions.h>
8#include <AK/CheckedFormatString.h>
9#include <AK/LexicalPath.h>
10#include <AK/NonnullOwnPtr.h>
11#include <AK/OwnPtr.h>
12#include <AK/Vector.h>
13#include <LibCore/System.h>
14#include <LibMain/Main.h>
15#include <dirent.h>
16#include <errno.h>
17#include <fcntl.h>
18#include <grp.h>
19#include <pwd.h>
20#include <stdio.h>
21#include <string.h>
22#include <sys/stat.h>
23#include <sys/types.h>
24#include <sys/wait.h>
25#include <unistd.h>
26
27bool g_follow_symlinks = false;
28bool g_there_was_an_error = false;
29bool g_have_seen_action_command = false;
30
31template<typename... Parameters>
32[[noreturn]] static void fatal_error(CheckedFormatString<Parameters...>&& fmtstr, Parameters const&... parameters)
33{
34 warn("\033[31m");
35 warn(move(fmtstr), parameters...);
36 warn("\033[0m");
37 warnln();
38 exit(1);
39}
40
41struct FileData {
42 // Full path to the file; either absolute or relative to cwd.
43 LexicalPath full_path;
44 // The parent directory of the file.
45 int dirfd { -1 };
46 // The file's basename, relative to the directory.
47 char const* basename { nullptr };
48 // Optionally, cached information as returned by stat/lstat/fstatat.
49 struct stat stat {
50 };
51 bool stat_is_valid : 1 { false };
52 // File type as returned from readdir(), or DT_UNKNOWN.
53 unsigned char d_type { DT_UNKNOWN };
54
55 const struct stat* ensure_stat()
56 {
57 if (stat_is_valid)
58 return &stat;
59
60 int flags = g_follow_symlinks ? 0 : AT_SYMLINK_NOFOLLOW;
61 int rc = fstatat(dirfd, basename, &stat, flags);
62 if (rc < 0) {
63 perror(full_path.string().characters());
64 g_there_was_an_error = true;
65 return nullptr;
66 }
67
68 stat_is_valid = true;
69
70 if (S_ISREG(stat.st_mode))
71 d_type = DT_REG;
72 else if (S_ISDIR(stat.st_mode))
73 d_type = DT_DIR;
74 else if (S_ISCHR(stat.st_mode))
75 d_type = DT_CHR;
76 else if (S_ISBLK(stat.st_mode))
77 d_type = DT_BLK;
78 else if (S_ISFIFO(stat.st_mode))
79 d_type = DT_FIFO;
80 else if (S_ISLNK(stat.st_mode))
81 d_type = DT_LNK;
82 else if (S_ISSOCK(stat.st_mode))
83 d_type = DT_SOCK;
84 else
85 VERIFY_NOT_REACHED();
86
87 return &stat;
88 }
89};
90
91class Command {
92public:
93 virtual ~Command() = default;
94 virtual bool evaluate(FileData& file_data) const = 0;
95};
96
97class StatCommand : public Command {
98public:
99 virtual bool evaluate(const struct stat&) const = 0;
100
101private:
102 virtual bool evaluate(FileData& file_data) const override
103 {
104 const struct stat* stat = file_data.ensure_stat();
105 if (!stat)
106 return false;
107 return evaluate(*stat);
108 }
109};
110
111class TypeCommand final : public Command {
112public:
113 TypeCommand(char const* arg)
114 {
115 StringView type { arg, strlen(arg) };
116 if (type.length() != 1 || !"bcdlpfs"sv.contains(type[0]))
117 fatal_error("Invalid mode: \033[1m{}", arg);
118 m_type = type[0];
119 }
120
121private:
122 virtual bool evaluate(FileData& file_data) const override
123 {
124 // First, make sure we have a type, but avoid calling
125 // sys$stat() unless we need to.
126 if (file_data.d_type == DT_UNKNOWN) {
127 if (file_data.ensure_stat() == nullptr)
128 return false;
129 }
130
131 auto type = file_data.d_type;
132 switch (m_type) {
133 case 'b':
134 return type == DT_BLK;
135 case 'c':
136 return type == DT_CHR;
137 case 'd':
138 return type == DT_DIR;
139 case 'l':
140 return type == DT_LNK;
141 case 'p':
142 return type == DT_FIFO;
143 case 'f':
144 return type == DT_REG;
145 case 's':
146 return type == DT_SOCK;
147 default:
148 // We've verified this is a correct character before.
149 VERIFY_NOT_REACHED();
150 }
151 }
152
153 char m_type { 0 };
154};
155
156class LinksCommand final : public StatCommand {
157public:
158 LinksCommand(char const* arg)
159 {
160 auto number = StringView { arg, strlen(arg) }.to_uint();
161 if (!number.has_value())
162 fatal_error("Invalid number: \033[1m{}", arg);
163 m_links = number.value();
164 }
165
166private:
167 virtual bool evaluate(const struct stat& stat) const override
168 {
169 return stat.st_nlink == m_links;
170 }
171
172 nlink_t m_links { 0 };
173};
174
175class UserCommand final : public StatCommand {
176public:
177 UserCommand(char const* arg)
178 {
179 if (struct passwd* passwd = getpwnam(arg)) {
180 m_uid = passwd->pw_uid;
181 } else {
182 // Attempt to parse it as decimal UID.
183 auto number = StringView { arg, strlen(arg) }.to_uint();
184 if (!number.has_value())
185 fatal_error("Invalid user: \033[1m{}", arg);
186 m_uid = number.value();
187 }
188 }
189
190private:
191 virtual bool evaluate(const struct stat& stat) const override
192 {
193 return stat.st_uid == m_uid;
194 }
195
196 uid_t m_uid { 0 };
197};
198
199class GroupCommand final : public StatCommand {
200public:
201 GroupCommand(char const* arg)
202 {
203 if (struct group* gr = getgrnam(arg)) {
204 m_gid = gr->gr_gid;
205 } else {
206 // Attempt to parse it as decimal GID.
207 auto number = StringView { arg, strlen(arg) }.to_int();
208 if (!number.has_value())
209 fatal_error("Invalid group: \033[1m{}", arg);
210 m_gid = number.value();
211 }
212 }
213
214private:
215 virtual bool evaluate(const struct stat& stat) const override
216 {
217 return stat.st_gid == m_gid;
218 }
219
220 gid_t m_gid { 0 };
221};
222
223class SizeCommand final : public StatCommand {
224public:
225 SizeCommand(char const* arg)
226 {
227 StringView view { arg, strlen(arg) };
228 if (view.ends_with('c')) {
229 m_is_bytes = true;
230 view = view.substring_view(0, view.length() - 1);
231 }
232 auto number = view.to_uint();
233 if (!number.has_value())
234 fatal_error("Invalid size: \033[1m{}", arg);
235 m_size = number.value();
236 }
237
238private:
239 virtual bool evaluate(const struct stat& stat) const override
240 {
241 if (m_is_bytes)
242 return stat.st_size == m_size;
243
244 auto size_divided_by_512_rounded_up = (stat.st_size + 511) / 512;
245 return size_divided_by_512_rounded_up == m_size;
246 }
247
248 off_t m_size { 0 };
249 bool m_is_bytes { false };
250};
251
252class NameCommand : public Command {
253public:
254 NameCommand(char const* pattern, CaseSensitivity case_sensitivity)
255 : m_pattern(pattern, strlen(pattern))
256 , m_case_sensitivity(case_sensitivity)
257 {
258 }
259
260private:
261 virtual bool evaluate(FileData& file_data) const override
262 {
263 return file_data.full_path.basename().matches(m_pattern, m_case_sensitivity);
264 }
265
266 StringView m_pattern;
267 CaseSensitivity m_case_sensitivity { CaseSensitivity::CaseSensitive };
268};
269
270class PrintCommand final : public Command {
271public:
272 PrintCommand(char terminator = '\n')
273 : m_terminator(terminator)
274 {
275 }
276
277private:
278 virtual bool evaluate(FileData& file_data) const override
279 {
280 out("{}{}", file_data.full_path, m_terminator);
281 return true;
282 }
283
284 char m_terminator { '\n' };
285};
286
287class ExecCommand final : public Command {
288public:
289 ExecCommand(Vector<char*>&& argv)
290 : m_argv(move(argv))
291 {
292 }
293
294private:
295 virtual bool evaluate(FileData& file_data) const override
296 {
297 pid_t pid = fork();
298
299 if (pid < 0) {
300 perror("fork");
301 g_there_was_an_error = true;
302 return false;
303 } else if (pid == 0) {
304 // Replace any occurrences of "{}" with the path. Since we're in the
305 // child and going to exec real soon, let's just const_cast away the
306 // constness.
307 auto argv = const_cast<Vector<char*>&>(m_argv);
308 for (auto& arg : argv) {
309 if (StringView { arg, strlen(arg) } == "{}")
310 arg = const_cast<char*>(file_data.full_path.string().characters());
311 }
312 argv.append(nullptr);
313 execvp(m_argv[0], argv.data());
314 perror("execvp");
315 exit(1);
316 } else {
317 int status;
318 int rc = waitpid(pid, &status, 0);
319 if (rc < 0) {
320 perror("waitpid");
321 g_there_was_an_error = true;
322 return false;
323 }
324 return WIFEXITED(status) && WEXITSTATUS(status) == 0;
325 }
326 }
327
328 Vector<char*> m_argv;
329};
330
331class AndCommand final : public Command {
332public:
333 AndCommand(NonnullOwnPtr<Command>&& lhs, NonnullOwnPtr<Command>&& rhs)
334 : m_lhs(move(lhs))
335 , m_rhs(move(rhs))
336 {
337 }
338
339private:
340 virtual bool evaluate(FileData& file_data) const override
341 {
342 return m_lhs->evaluate(file_data) && m_rhs->evaluate(file_data);
343 }
344
345 NonnullOwnPtr<Command> m_lhs;
346 NonnullOwnPtr<Command> m_rhs;
347};
348
349class OrCommand final : public Command {
350public:
351 OrCommand(NonnullOwnPtr<Command>&& lhs, NonnullOwnPtr<Command>&& rhs)
352 : m_lhs(move(lhs))
353 , m_rhs(move(rhs))
354 {
355 }
356
357private:
358 virtual bool evaluate(FileData& file_data) const override
359 {
360 return m_lhs->evaluate(file_data) || m_rhs->evaluate(file_data);
361 }
362
363 NonnullOwnPtr<Command> m_lhs;
364 NonnullOwnPtr<Command> m_rhs;
365};
366
367static OwnPtr<Command> parse_complex_command(Vector<char*>& args);
368
369// Parse a simple command starting at optind; leave optind at its the last
370// argument. Return nullptr if we reach the end of arguments.
371static OwnPtr<Command> parse_simple_command(Vector<char*>& args)
372{
373 if (args.is_empty())
374 return {};
375
376 char* raw_arg = args.take_first();
377 StringView arg { raw_arg, strlen(raw_arg) };
378
379 if (arg == "(") {
380 auto command = parse_complex_command(args);
381 if (command && !args.is_empty() && StringView { args.first(), strlen(args.first()) } == ")")
382 return command;
383 fatal_error("Unmatched \033[1m(");
384 } else if (arg == "-type") {
385 if (args.is_empty())
386 fatal_error("-type: requires additional arguments");
387 return make<TypeCommand>(args.take_first());
388 } else if (arg == "-links") {
389 if (args.is_empty())
390 fatal_error("-links: requires additional arguments");
391 return make<LinksCommand>(args.take_first());
392 } else if (arg == "-user") {
393 if (args.is_empty())
394 fatal_error("-user: requires additional arguments");
395 return make<UserCommand>(args.take_first());
396 } else if (arg == "-group") {
397 if (args.is_empty())
398 fatal_error("-group: requires additional arguments");
399 return make<GroupCommand>(args.take_first());
400 } else if (arg == "-size") {
401 if (args.is_empty())
402 fatal_error("-size: requires additional arguments");
403 return make<SizeCommand>(args.take_first());
404 } else if (arg == "-name") {
405 if (args.is_empty())
406 fatal_error("-name: requires additional arguments");
407 return make<NameCommand>(args.take_first(), CaseSensitivity::CaseSensitive);
408 } else if (arg == "-iname") {
409 if (args.is_empty())
410 fatal_error("-iname: requires additional arguments");
411 return make<NameCommand>(args.take_first(), CaseSensitivity::CaseInsensitive);
412 } else if (arg == "-print") {
413 g_have_seen_action_command = true;
414 return make<PrintCommand>();
415 } else if (arg == "-print0") {
416 g_have_seen_action_command = true;
417 return make<PrintCommand>(0);
418 } else if (arg == "-exec") {
419 if (args.is_empty())
420 fatal_error("-exec: requires additional arguments");
421 g_have_seen_action_command = true;
422 Vector<char*> command_argv;
423 while (!args.is_empty()) {
424 char* next = args.take_first();
425 if (next[0] == ';')
426 break;
427 command_argv.append(next);
428 }
429 return make<ExecCommand>(move(command_argv));
430 } else {
431 fatal_error("Unsupported command \033[1m{}", arg);
432 }
433}
434
435static OwnPtr<Command> parse_complex_command(Vector<char*>& args)
436{
437 auto command = parse_simple_command(args);
438
439 while (command && !args.is_empty()) {
440 char* raw_arg = args.take_first();
441 StringView arg { raw_arg, strlen(raw_arg) };
442
443 enum {
444 And,
445 Or,
446 } binary_operation { And };
447
448 if (arg == "-a") {
449 binary_operation = And;
450 } else if (arg == "-o") {
451 binary_operation = Or;
452 } else if (arg == ")") {
453 // Ooops, looked too far.
454 args.prepend(raw_arg);
455 return command;
456 } else {
457 // Juxtaposition is an And too, and there's nothing to skip.
458 args.prepend(raw_arg);
459 binary_operation = And;
460 }
461
462 auto rhs = parse_complex_command(args);
463 if (!rhs)
464 fatal_error("Missing right-hand side");
465
466 if (binary_operation == And)
467 command = make<AndCommand>(command.release_nonnull(), rhs.release_nonnull());
468 else
469 command = make<OrCommand>(command.release_nonnull(), rhs.release_nonnull());
470 }
471
472 return command;
473}
474
475static NonnullOwnPtr<Command> parse_all_commands(Vector<char*>& args)
476{
477 auto command = parse_complex_command(args);
478
479 if (g_have_seen_action_command) {
480 VERIFY(command);
481 return command.release_nonnull();
482 }
483
484 if (!command) {
485 return make<PrintCommand>();
486 }
487
488 return make<AndCommand>(command.release_nonnull(), make<PrintCommand>());
489}
490
491static void walk_tree(FileData& root_data, Command& command)
492{
493 command.evaluate(root_data);
494
495 // We should try to read directory entries if either:
496 // * This is a directory.
497 // * This is a symlink (that could point to a directory),
498 // and we're following symlinks.
499 // * The type is unknown, so it could be a directory.
500 switch (root_data.d_type) {
501 case DT_DIR:
502 case DT_UNKNOWN:
503 break;
504 case DT_LNK:
505 if (g_follow_symlinks)
506 break;
507 return;
508 default:
509 return;
510 }
511
512 int dirfd = openat(root_data.dirfd, root_data.basename, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
513 if (dirfd < 0) {
514 if (errno == ENOTDIR) {
515 // Above we decided to try to open this file because it could
516 // be a directory, but turns out it's not. This is fine though.
517 return;
518 }
519 perror(root_data.full_path.string().characters());
520 g_there_was_an_error = true;
521 return;
522 }
523
524 DIR* dir = fdopendir(dirfd);
525
526 while (true) {
527 errno = 0;
528 auto* dirent = readdir(dir);
529 if (!dirent)
530 break;
531
532 if (strcmp(dirent->d_name, ".") == 0 || strcmp(dirent->d_name, "..") == 0)
533 continue;
534
535 FileData file_data {
536 root_data.full_path.append({ dirent->d_name, strlen(dirent->d_name) }),
537 dirfd,
538 dirent->d_name,
539 (struct stat) {},
540 false,
541 dirent->d_type,
542 };
543 walk_tree(file_data, command);
544 }
545
546 if (errno != 0) {
547 perror(root_data.full_path.string().characters());
548 g_there_was_an_error = true;
549 }
550
551 closedir(dir);
552}
553
554ErrorOr<int> serenity_main(Main::Arguments arguments)
555{
556 Vector<char*> args;
557 args.append(arguments.argv + 1, arguments.argc - 1);
558
559 OwnPtr<Command> command;
560 Vector<LexicalPath> paths;
561
562 while (!args.is_empty()) {
563 char* raw_arg = args.take_first();
564 StringView arg { raw_arg, strlen(raw_arg) };
565 if (arg == "-L") {
566 g_follow_symlinks = true;
567 } else if (!arg.starts_with('-')) {
568 paths.append(LexicalPath(arg));
569 } else {
570 // No special case, so add back the argument and try to parse a command.
571 args.prepend(raw_arg);
572 command = parse_all_commands(args);
573 }
574 }
575
576 if (!command)
577 command = make<PrintCommand>();
578
579 if (paths.is_empty())
580 paths.append(LexicalPath("."));
581
582 for (auto& path : paths) {
583 DeprecatedString dirname = path.dirname();
584 DeprecatedString basename = path.basename();
585
586 int dirfd = TRY(Core::System::open(dirname, O_RDONLY | O_DIRECTORY | O_CLOEXEC));
587
588 FileData file_data {
589 path,
590 dirfd,
591 basename.characters(),
592 (struct stat) {},
593 false,
594 DT_UNKNOWN,
595 };
596 walk_tree(file_data, *command);
597 close(dirfd);
598 }
599
600 return g_there_was_an_error ? 1 : 0;
601}