Serenity Operating System
1/*
2 * Copyright (c) 2022, the SerenityOS developers.
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include <AK/CharacterTypes.h>
8#include <AK/Forward.h>
9#include <LibCore/ArgsParser.h>
10#include <LibCore/File.h>
11#include <LibCore/System.h>
12#include <LibMain/Main.h>
13#include <unistd.h>
14
15enum class StringOffsetFormat {
16 None = 0,
17 Decimal,
18 Octal,
19 Hexadecimal
20};
21
22// NOTE: This is similar to how the cat utility works in the sense of aggregating
23// data in 32K buffer.
24static constexpr size_t buffer_read_size = 32768;
25
26static bool should_print_characters(Vector<u8> const& characters)
27{
28 for (u8 ch : characters) {
29 if (is_ascii_printable(ch) && !is_ascii_space(ch))
30 return true;
31 }
32 return false;
33}
34
35static void print_characters(Vector<u8> const& characters, StringOffsetFormat string_offset_format, size_t string_offset_position)
36{
37 switch (string_offset_format) {
38 case StringOffsetFormat::Decimal:
39 out("{:>7d} ", string_offset_position);
40 break;
41 case StringOffsetFormat::Octal:
42 out("{:>7o} ", string_offset_position);
43 break;
44 case StringOffsetFormat::Hexadecimal:
45 out("{:>7x} ", string_offset_position);
46 break;
47 default:
48 break;
49 }
50 outln("{:s}", characters.span());
51}
52
53static int process_characters_in_span(Vector<u8>& characters, ReadonlyBytes span)
54{
55 int processed_characters = 0;
56 for (u8 ch : span) {
57 ++processed_characters;
58 if (is_ascii_printable(ch) || ch == '\t')
59 characters.append(ch);
60 else
61 break;
62 }
63 return processed_characters;
64}
65
66static ErrorOr<void> process_strings_in_file(StringView path, bool show_paths, StringOffsetFormat string_offset_format, size_t minimum_string_length)
67{
68 Array<u8, buffer_read_size> buffer;
69 Vector<u8> output_characters;
70 auto file = TRY(Core::File::open_file_or_standard_stream(path, Core::File::OpenMode::Read));
71 size_t processed_characters = 0;
72 size_t string_offset_position = 0;
73 bool did_show_path = false;
74 while (!file->is_eof()) {
75 auto buffer_span = TRY(file->read_some(buffer));
76 while (!buffer_span.is_empty()) {
77 string_offset_position += processed_characters;
78 processed_characters = process_characters_in_span(output_characters, buffer_span);
79 if (show_paths && !did_show_path) {
80 outln("path {}:", path);
81 did_show_path = true;
82 }
83 if (output_characters.size() >= minimum_string_length && should_print_characters(output_characters)) {
84 print_characters(output_characters, string_offset_format, string_offset_position);
85 }
86 buffer_span = buffer_span.slice(processed_characters);
87 output_characters.clear();
88 }
89 }
90 return {};
91}
92
93ErrorOr<int> serenity_main(Main::Arguments arguments)
94{
95 TRY(Core::System::pledge("stdio rpath"));
96
97 Vector<StringView> paths;
98 size_t minimum_string_length = 4;
99 bool show_paths = false;
100
101 StringOffsetFormat string_offset_format { StringOffsetFormat::None };
102
103 Core::ArgsParser args_parser;
104 args_parser.add_option(minimum_string_length, "Specify the minimum string length.", nullptr, 'n', "number");
105 args_parser.add_option(show_paths, "Display the path for each matched file.", nullptr, 'p');
106 args_parser.add_option({ Core::ArgsParser::OptionArgumentMode::Required,
107 "Write offset relative to start of each file in (d)ec, (o)ct, or he(x) format.",
108 nullptr,
109 't',
110 "format",
111 [&string_offset_format](StringView value) {
112 if (value == "d") {
113 string_offset_format = StringOffsetFormat::Decimal;
114 } else if (value == "o") {
115 string_offset_format = StringOffsetFormat::Octal;
116 } else if (value == "x") {
117 string_offset_format = StringOffsetFormat::Hexadecimal;
118 } else {
119 return false;
120 }
121 return true;
122 } });
123 args_parser.set_general_help("Write the sequences of printable characters in files or pipes to stdout.");
124 args_parser.add_positional_argument(paths, "File path", "path", Core::ArgsParser::Required::No);
125 args_parser.parse(arguments);
126
127 if (minimum_string_length < 1) {
128 warnln("Invalid minimum string length {}", minimum_string_length);
129 return 1;
130 }
131
132 if (paths.is_empty())
133 paths.append("-"sv);
134
135 for (auto const& path : paths)
136 TRY(process_strings_in_file(path, show_paths, string_offset_format, minimum_string_length));
137
138 return 0;
139}