Serenity Operating System
1/*
2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 *
8 * 1. Redistributions of source code must retain the above copyright notice, this
9 * list of conditions and the following disclaimer.
10 *
11 * 2. Redistributions in binary form must reproduce the above copyright notice,
12 * this list of conditions and the following disclaimer in the documentation
13 * and/or other materials provided with the distribution.
14 *
15 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 */
26
27#include <AK/HashMap.h>
28#include <AK/QuickSort.h>
29#include <AK/String.h>
30#include <AK/StringBuilder.h>
31#include <AK/Vector.h>
32#include <LibCore/ArgsParser.h>
33#include <LibCore/DateTime.h>
34#include <LibCore/DirIterator.h>
35#include <ctype.h>
36#include <dirent.h>
37#include <errno.h>
38#include <fcntl.h>
39#include <grp.h>
40#include <pwd.h>
41#include <stdio.h>
42#include <string.h>
43#include <sys/ioctl.h>
44#include <sys/stat.h>
45#include <time.h>
46#include <unistd.h>
47
48static int do_file_system_object_long(const char* path);
49static int do_file_system_object_short(const char* path);
50
51static bool flag_colorize = true;
52static bool flag_long = false;
53static bool flag_show_dotfiles = false;
54static bool flag_show_inode = false;
55static bool flag_print_numeric = false;
56static bool flag_human_readable = false;
57static bool flag_sort_by_timestamp = false;
58static bool flag_reverse_sort = false;
59
60static size_t terminal_rows = 0;
61static size_t terminal_columns = 0;
62static bool output_is_terminal = false;
63
64static HashMap<uid_t, String> users;
65static HashMap<gid_t, String> groups;
66
67int main(int argc, char** argv)
68{
69 if (pledge("stdio rpath tty", nullptr) < 0) {
70 perror("pledge");
71 return 1;
72 }
73
74 struct winsize ws;
75 int rc = ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws);
76 if (rc == 0) {
77 terminal_rows = ws.ws_row;
78 terminal_columns = ws.ws_col;
79 output_is_terminal = true;
80 }
81
82 if (pledge("stdio rpath", nullptr) < 0) {
83 perror("pledge");
84 return 1;
85 }
86
87 Vector<const char*> paths;
88
89 Core::ArgsParser args_parser;
90 args_parser.add_option(flag_show_dotfiles, "Show dotfiles", "all", 'a');
91 args_parser.add_option(flag_long, "Display long info", "long", 'l');
92 args_parser.add_option(flag_sort_by_timestamp, "Sort files by timestamp", nullptr, 't');
93 args_parser.add_option(flag_reverse_sort, "Reverse sort order", "reverse", 'r');
94 args_parser.add_option(flag_colorize, "Use pretty colors", nullptr, 'G');
95 args_parser.add_option(flag_show_inode, "Show inode ids", "inode", 'i');
96 args_parser.add_option(flag_print_numeric, "In long format, display numeric UID/GID", "numeric-uid-gid", 'n');
97 args_parser.add_option(flag_human_readable, "Print human-readable sizes", "human-readable", 'h');
98 args_parser.add_positional_argument(paths, "Directory to list", "path", Core::ArgsParser::Required::No);
99 args_parser.parse(argc, argv);
100
101 if (flag_long) {
102 setpwent();
103 for (auto* pwd = getpwent(); pwd; pwd = getpwent())
104 users.set(pwd->pw_uid, pwd->pw_name);
105 endpwent();
106 setgrent();
107 for (auto* grp = getgrent(); grp; grp = getgrent())
108 groups.set(grp->gr_gid, grp->gr_name);
109 endgrent();
110 }
111
112 auto do_file_system_object = [&](const char* path) {
113 if (flag_long)
114 return do_file_system_object_long(path);
115 return do_file_system_object_short(path);
116 };
117
118 int status = 0;
119 if (paths.is_empty()) {
120 status = do_file_system_object(".");
121 } else if (paths.size() == 1) {
122 status = do_file_system_object(paths[0]);
123 } else {
124 for (auto& path : paths) {
125 printf("%s:\n", path);
126 status = do_file_system_object(path);
127 }
128 }
129 return status;
130}
131
132int print_escaped(const char* name)
133{
134 int printed = 0;
135
136 for (int i = 0; name[i] != '\0'; i++) {
137 if (isprint(name[i])) {
138 putchar(name[i]);
139 printed++;
140 } else {
141 printed += printf("\\%03d", name[i]);
142 }
143 }
144
145 return printed;
146}
147
148size_t print_name(const struct stat& st, const String& name, const char* path_for_link_resolution = nullptr)
149{
150 size_t nprinted = 0;
151
152 if (!flag_colorize || !output_is_terminal) {
153 nprinted = printf("%s", name.characters());
154 } else {
155 const char* begin_color = "";
156 const char* end_color = "\033[0m";
157
158 if (st.st_mode & S_ISVTX)
159 begin_color = "\033[42;30;1m";
160 else if (st.st_mode & S_ISUID)
161 begin_color = "\033[41;1m";
162 else if (S_ISLNK(st.st_mode))
163 begin_color = "\033[36;1m";
164 else if (S_ISDIR(st.st_mode))
165 begin_color = "\033[34;1m";
166 else if (st.st_mode & 0111)
167 begin_color = "\033[32;1m";
168 else if (S_ISSOCK(st.st_mode))
169 begin_color = "\033[35;1m";
170 else if (S_ISCHR(st.st_mode) || S_ISBLK(st.st_mode))
171 begin_color = "\033[33;1m";
172 printf("%s", begin_color);
173 nprinted = print_escaped(name.characters());
174 printf("%s", end_color);
175 }
176 if (S_ISLNK(st.st_mode)) {
177 if (path_for_link_resolution) {
178 char linkbuf[PATH_MAX];
179 ssize_t nread = readlink(path_for_link_resolution, linkbuf, sizeof(linkbuf));
180 if (nread < 0)
181 perror("readlink failed");
182 else
183 nprinted += printf(" -> ") + print_escaped(linkbuf);
184 } else {
185 nprinted += printf("@");
186 }
187 } else if (S_ISDIR(st.st_mode)) {
188 nprinted += printf("/");
189 } else if (st.st_mode & 0111) {
190 nprinted += printf("*");
191 }
192 return nprinted;
193}
194
195// FIXME: Remove this hackery once printf() supports floats.
196// FIXME: Also, we should probably round the sizes in ls -lh output.
197static String number_string_with_one_decimal(float number, const char* suffix)
198{
199 float decimals = number - (int)number;
200 return String::format("%d.%d%s", (int)number, (int)(decimals * 10), suffix);
201}
202
203static String human_readable_size(size_t size)
204{
205 if (size < 1 * KB)
206 return String::number(size);
207 if (size < 1 * MB)
208 return number_string_with_one_decimal((float)size / (float)KB, "K");
209 if (size < 1 * GB)
210 return number_string_with_one_decimal((float)size / (float)MB, "M");
211 return number_string_with_one_decimal((float)size / (float)GB, "G");
212}
213
214bool print_filesystem_object(const String& path, const String& name, const struct stat& st)
215{
216 if (flag_show_inode)
217 printf("%08u ", st.st_ino);
218
219 if (S_ISDIR(st.st_mode))
220 printf("d");
221 else if (S_ISLNK(st.st_mode))
222 printf("l");
223 else if (S_ISBLK(st.st_mode))
224 printf("b");
225 else if (S_ISCHR(st.st_mode))
226 printf("c");
227 else if (S_ISFIFO(st.st_mode))
228 printf("f");
229 else if (S_ISSOCK(st.st_mode))
230 printf("s");
231 else if (S_ISREG(st.st_mode))
232 printf("-");
233 else
234 printf("?");
235
236 printf("%c%c%c%c%c%c%c%c",
237 st.st_mode & S_IRUSR ? 'r' : '-',
238 st.st_mode & S_IWUSR ? 'w' : '-',
239 st.st_mode & S_ISUID ? 's' : (st.st_mode & S_IXUSR ? 'x' : '-'),
240 st.st_mode & S_IRGRP ? 'r' : '-',
241 st.st_mode & S_IWGRP ? 'w' : '-',
242 st.st_mode & S_ISGID ? 's' : (st.st_mode & S_IXGRP ? 'x' : '-'),
243 st.st_mode & S_IROTH ? 'r' : '-',
244 st.st_mode & S_IWOTH ? 'w' : '-');
245
246 if (st.st_mode & S_ISVTX)
247 printf("t");
248 else
249 printf("%c", st.st_mode & S_IXOTH ? 'x' : '-');
250
251 auto username = users.get(st.st_uid);
252 auto groupname = groups.get(st.st_gid);
253 if (!flag_print_numeric && username.has_value()) {
254 printf(" %7s", username.value().characters());
255 } else {
256 printf(" %7u", st.st_uid);
257 }
258 if (!flag_print_numeric && groupname.has_value()) {
259 printf(" %7s", groupname.value().characters());
260 } else {
261 printf(" %7u", st.st_gid);
262 }
263
264 if (S_ISCHR(st.st_mode) || S_ISBLK(st.st_mode)) {
265 printf(" %4u,%4u ", major(st.st_rdev), minor(st.st_rdev));
266 } else {
267 if (flag_human_readable) {
268 ASSERT(st.st_size > 0);
269 printf(" %10s ", human_readable_size((size_t)st.st_size).characters());
270 } else {
271 printf(" %10u ", st.st_size);
272 }
273 }
274
275 printf(" %s ", Core::DateTime::from_timestamp(st.st_mtime).to_string().characters());
276
277 print_name(st, name, path.characters());
278
279 printf("\n");
280 return true;
281}
282
283int do_file_system_object_long(const char* path)
284{
285 Core::DirIterator di(path, !flag_show_dotfiles ? Core::DirIterator::SkipDots : Core::DirIterator::Flags::NoFlags);
286 if (di.has_error()) {
287 if (di.error() == ENOTDIR) {
288 struct stat stat;
289 int rc = lstat(path, &stat);
290 if (rc < 0) {
291 perror("lstat");
292 memset(&stat, 0, sizeof(stat));
293 }
294 if (print_filesystem_object(path, path, stat))
295 return 0;
296 return 2;
297 }
298 fprintf(stderr, "%s: %s\n", path, di.error_string());
299 return 1;
300 }
301
302 struct FileMetadata {
303 String name;
304 String path;
305 struct stat stat;
306 };
307
308 Vector<FileMetadata> files;
309 while (di.has_next()) {
310 FileMetadata metadata;
311 metadata.name = di.next_path();
312 ASSERT(!metadata.name.is_empty());
313 if (metadata.name[0] == '.' && !flag_show_dotfiles)
314 continue;
315 StringBuilder builder;
316 builder.append(path);
317 builder.append('/');
318 builder.append(metadata.name);
319 metadata.path = builder.to_string();
320 ASSERT(!metadata.path.is_null());
321 int rc = lstat(metadata.path.characters(), &metadata.stat);
322 if (rc < 0) {
323 perror("lstat");
324 memset(&metadata.stat, 0, sizeof(metadata.stat));
325 }
326 files.append(move(metadata));
327 }
328
329 quick_sort(files.begin(), files.end(), [](auto& a, auto& b) {
330 if (flag_sort_by_timestamp) {
331 if (flag_reverse_sort)
332 return a.stat.st_mtime > b.stat.st_mtime;
333 return a.stat.st_mtime < b.stat.st_mtime;
334 }
335 // Fine, sort by name then!
336 if (flag_reverse_sort)
337 return a.name > b.name;
338 return a.name < b.name;
339 });
340
341 for (auto& file : files) {
342 if (!print_filesystem_object(file.path, file.name, file.stat))
343 return 2;
344 }
345 return 0;
346}
347
348bool print_filesystem_object_short(const char* path, const char* name, size_t* nprinted)
349{
350 struct stat st;
351 int rc = lstat(path, &st);
352 if (rc == -1) {
353 printf("lstat(%s) failed: %s\n", path, strerror(errno));
354 return false;
355 }
356
357 *nprinted = print_name(st, name);
358 return true;
359}
360
361int do_file_system_object_short(const char* path)
362{
363 Core::DirIterator di(path, !flag_show_dotfiles ? Core::DirIterator::SkipDots : Core::DirIterator::Flags::NoFlags);
364 if (di.has_error()) {
365 if (di.error() == ENOTDIR) {
366 size_t nprinted = 0;
367 bool status = print_filesystem_object_short(path, path, &nprinted);
368 printf("\n");
369 if (status)
370 return 0;
371 return 2;
372 }
373 fprintf(stderr, "%s: %s\n", path, di.error_string());
374 return 1;
375 }
376
377 Vector<String> names;
378 size_t longest_name = 0;
379 while (di.has_next()) {
380 String name = di.next_path();
381 names.append(name);
382 if (names.last().length() > longest_name)
383 longest_name = name.length();
384 }
385 quick_sort(names.begin(), names.end(), [](auto& a, auto& b) { return a < b; });
386
387 size_t printed_on_row = 0;
388 size_t nprinted = 0;
389 for (size_t i = 0; i < names.size(); ++i) {
390 auto& name = names[i];
391 StringBuilder builder;
392 builder.append(path);
393 builder.append('/');
394 builder.append(name);
395 if (!print_filesystem_object_short(builder.to_string().characters(), name.characters(), &nprinted))
396 return 2;
397 int offset = 0;
398 if (terminal_columns > longest_name)
399 offset = terminal_columns % longest_name / (terminal_columns / longest_name);
400
401 // The offset must be at least 2 because:
402 // - With each file an additional char is printed e.g. '@','*'.
403 // - Each filename must be separated by a space.
404 size_t column_width = longest_name + (offset > 0 ? offset : 2);
405 printed_on_row += column_width;
406
407 for (size_t j = nprinted; i != (names.size() - 1) && j < column_width; ++j)
408 printf(" ");
409 if ((printed_on_row + column_width) >= terminal_columns) {
410 printf("\n");
411 printed_on_row = 0;
412 }
413 }
414 if (printed_on_row)
415 printf("\n");
416 return 0;
417}