back to scratko.xyz
aboutsummaryrefslogtreecommitdiff
path: root/ls_imitation.c
diff options
context:
space:
mode:
Diffstat (limited to 'ls_imitation.c')
-rw-r--r--ls_imitation.c620
1 files changed, 620 insertions, 0 deletions
diff --git a/ls_imitation.c b/ls_imitation.c
new file mode 100644
index 0000000..e396efa
--- /dev/null
+++ b/ls_imitation.c
@@ -0,0 +1,620 @@
+/*
+ * ls_imitation
+ *
+ * A simplified implementation of the Unix ls -l command.
+ * Lists files and directories with detailed information including:
+ * file type and permissions
+ * ownership (user and group)
+ * number of hard links
+ * size or device numbers
+ * modification date
+ * symbolic link target (if any)
+ *
+ * Colors are shown when output is to a terminal.
+ *
+ * Author: Andrey Kuznetsov
+ */
+
+#include "data_struct.h"
+#include "columns.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <dirent.h>
+#include <sys/stat.h>
+#include <pwd.h>
+#include <grp.h>
+#include <time.h>
+#include <string.h>
+#include <errno.h>
+#include <unistd.h>
+#include <sys/sysmacros.h>
+
+/* ANSI color codes for different file types */
+#define RESET "\033[0m"
+#define DIR_COLOR "\033[1;34m"
+#define EXEC_COLOR "\033[1;32m"
+#define SYMLINK_COLOR "\033[1;36m"
+#define DEVICE_COLOR "\033[1;33m"
+#define SOCK_COLOR "\033[1;35m"
+#define FIFO_COLOR "\033[33m"
+#define SUID_COLOR "\033[37;41m"
+#define SGID_COLOR "\033[30;43m"
+#define STICKY_COLOR "\033[37;44m"
+#define STICKY_OTHER_WRITABLE "\033[30;42m"
+
+enum permissions_t {
+ user_p, group_p, other_p
+};
+
+enum {
+ half_year_time = 15552000, /* Half a year in seconds */
+ block_size = 512,
+ display_block_size = 1024,
+ min_args_for_header = 2 /* minimum number of paths required to show
+ directory headers */
+};
+
+enum arg_type {
+ arg_file,
+ arg_directory
+};
+
+struct permissions_mask {
+ mode_t owner_rights;
+ mode_t group_rights;
+ mode_t other_rights;
+};
+
+struct line_builder {
+ char *line_buffer;
+ size_t offset;
+ const char *color; /* ANSI color escape sequence */
+ const char *link_target; /* NULL if not symbolic link. Otherwise points to
+ link target */
+ int device_flag; /* Non-zero if file is block or char device */
+};
+
+struct print_options {
+ int show_directory_header;
+ int output_to_tty;
+};
+
+/* Bit masks for read, write, execute checks */
+static const int r_bits[] = { S_IRUSR, S_IRGRP, S_IROTH };
+static const int w_bits[] = { S_IWUSR, S_IWGRP, S_IWOTH };
+static const int x_bits[] = { S_IXUSR, S_IXGRP, S_IXOTH };
+
+/*
+ * Append -> target suffix to symlink.
+ * Assumes line->line_buffer already contains filename and newline.
+ * checks for line->link_target are performed here.
+ * offset-1 remove '\n' symbol that append_file_name() is added
+ */
+static void append_symlink_name(struct line_builder *line)
+{
+ if(line->offset >= buffer_size)
+ return;
+
+ if(line->link_target)
+ line->offset += snprintf(line->line_buffer + line->offset-1,
+ buffer_size - line->offset-1, " -> %s\n",
+ line->link_target);
+}
+
+/* Checks whether stdout is a terminal (tty) to adjust output formatting */
+int check_output_to_tty()
+{
+ return isatty(STDOUT_FILENO);
+}
+
+/* Append colored filename to builder buffer */
+static void append_file_name(struct line_builder *line,
+ const char *file_name,
+ const struct print_options *opts)
+{
+ if(line->offset >= buffer_size)
+ return;
+
+ if(!opts->output_to_tty)
+ line->color = "";
+
+ line->offset +=
+ snprintf(line->line_buffer + line->offset, buffer_size - line->offset,
+ " %s%s" RESET "\n", line->color, file_name);
+}
+
+/*
+ * If mtime is more than 6 months ago, print "M D Y"
+ * otherwise print "M D H:M"
+ */
+static void append_modification_date(struct line_builder *line,
+ const struct stat *file_info)
+{
+ time_t current_time = time(NULL);
+ struct tm *full_time = localtime(&file_info->st_mtime);
+
+ if(full_time == NULL)
+ return;
+
+ if(line->offset >= buffer_size)
+ return;
+
+ if((current_time - file_info->st_mtime) > half_year_time)
+ line->offset += strftime(line->line_buffer + line->offset,
+ buffer_size - line->offset,
+ "%b %e %Y", full_time);
+ else
+ line->offset += strftime(line->line_buffer + line->offset,
+ buffer_size - line->offset,
+ "%b %e %H:%M", full_time);
+}
+
+int is_write_other_bit_is_set(mode_t permissions_mask, enum permissions_t mode)
+{
+ return mode == other_p && permissions_mask & w_bits[mode];
+}
+
+/* Append execute bit x for user, group, other. Handles SUID, SGID, STICKY
+ * and assigns color when special bits are set
+ */
+static void append_x_bit(struct line_builder *line, mode_t permissions_mask,
+ enum permissions_t mode, mode_t file_mode,
+ int write_other_bit_flag)
+{
+ int symbol;
+
+ switch(mode) {
+ case user_p:
+ if(file_mode & S_ISUID) {
+ symbol = permissions_mask & x_bits[mode] ? 's' : 'S';
+ line->offset +=
+ snprintf(line->line_buffer + line->offset,
+ buffer_size - line->offset, "%c", symbol);
+ line->color = SUID_COLOR;
+ } else {
+ line->offset +=
+ snprintf(line->line_buffer + line->offset,
+ buffer_size - line->offset, "%c",
+ permissions_mask & x_bits[mode] ? 'x' : '-');
+ }
+ break;
+ case group_p:
+ if(file_mode & S_ISGID) {
+ symbol = permissions_mask & x_bits[mode] ? 's' : 'S';
+ line->offset +=
+ snprintf(line->line_buffer + line->offset,
+ buffer_size - line->offset, "%c", symbol);
+ line->color = SGID_COLOR;
+ } else {
+ line->offset +=
+ snprintf(line->line_buffer + line->offset,
+ buffer_size - line->offset, "%c",
+ permissions_mask & x_bits[mode] ? 'x' : '-');
+ }
+ break;
+ case other_p:
+ if(file_mode & S_ISVTX) {
+ symbol = permissions_mask & x_bits[mode] ? 't' : 'T';
+ line->offset +=
+ snprintf(line->line_buffer + line->offset,
+ buffer_size - line->offset, "%c", symbol);
+ line->color =
+ write_other_bit_flag ? STICKY_OTHER_WRITABLE : STICKY_COLOR;
+ } else {
+ line->offset +=
+ snprintf(line->line_buffer + line->offset,
+ buffer_size - line->offset, "%c",
+ permissions_mask & x_bits[mode] ? 'x' : '-');
+ }
+ }
+}
+
+/* Append rwx permissions for one user category (user, group, other) */
+static void append_permissions(struct line_builder *line,
+ mode_t permissions_mask, enum permissions_t who,
+ mode_t file_mode)
+{
+ int write_other_bit_flag = 0;
+
+ line->offset +=
+ snprintf(line->line_buffer + line->offset, buffer_size - line->offset,
+ "%c", permissions_mask & r_bits[who] ? 'r' : '-');
+ line->offset +=
+ snprintf(line->line_buffer + line->offset, buffer_size - line->offset,
+ "%c", permissions_mask & w_bits[who] ? 'w' : '-');
+
+ write_other_bit_flag = is_write_other_bit_is_set(permissions_mask, who);
+ append_x_bit(line, permissions_mask, who, file_mode,
+ write_other_bit_flag);
+}
+
+/*
+ * Append owner and group names retrieved from UID, GID.
+ * Uses getpwuid and getgrgid
+ */
+static void append_owner_and_group(struct line_builder *line,
+ const struct stat *file_info)
+{
+ struct passwd *user = getpwuid(file_info->st_uid);
+ struct group *group = getgrgid(file_info->st_gid);
+
+ if(user == NULL || group == NULL)
+ return;
+
+ line->offset +=
+ snprintf(line->line_buffer + line->offset, buffer_size - line->offset,
+ " %s %s", user->pw_name, group->gr_name);
+}
+
+/* Append device major and minor or file size */
+static void append_size_or_device(struct line_builder *line,
+ const struct stat *file_info)
+{
+ int major_dev, minor_dev;
+
+ if(line->offset >= buffer_size)
+ return;
+
+ if(line->device_flag) {
+ major_dev = major(file_info->st_rdev);
+ minor_dev = minor(file_info->st_rdev);
+
+ line->offset +=
+ snprintf(line->line_buffer + line->offset,
+ buffer_size - line->offset, " %u, %u ", major_dev,
+ minor_dev);
+ } else
+ line->offset +=
+ snprintf(line->line_buffer + line->offset,
+ buffer_size - line->offset, " %lu ",file_info->st_size);
+}
+
+/* Append number of hard links */
+static void append_hard_links(struct line_builder *line,
+ const struct stat *file_info)
+{
+ line->offset +=
+ snprintf(line->line_buffer + line->offset, buffer_size - line->offset,
+ " %ld", file_info->st_nlink);
+}
+
+/* Return true if any execute bit is set for regular file */
+static int is_reg_executable_file(const struct permissions_mask *permissions)
+{
+ return
+ permissions->owner_rights & x_bits[user_p] ||
+ permissions->group_rights & x_bits[group_p] ||
+ permissions->other_rights & x_bits[other_p];
+}
+
+/* Append file type character and set color depending on type */
+static void append_file_type(struct line_builder *line,
+ const mode_t file_mode,
+ const struct permissions_mask *permissions)
+{
+ if(S_ISREG(file_mode)) {
+ line->offset += snprintf(line->line_buffer, buffer_size, "%c", '-');
+ line->color = is_reg_executable_file(permissions) ? EXEC_COLOR : RESET;
+
+ } else if(S_ISDIR(file_mode)) {
+ line->offset += snprintf(line->line_buffer + line->offset,
+ buffer_size - line->offset, "%c", 'd');
+ line->color = DIR_COLOR;
+
+ } else if(line->link_target) {
+ line->offset += snprintf(line->line_buffer + line->offset ,
+ buffer_size - line->offset, "%c", 'l');
+ line->color = SYMLINK_COLOR;
+
+ } else if(S_ISBLK(file_mode)) {
+ line->offset += snprintf(line->line_buffer + line->offset,
+ buffer_size - line->offset, "%c", 'b');
+ line->device_flag = 1;
+ line->color = DEVICE_COLOR;
+
+ } else if(S_ISCHR(file_mode)) {
+ line->offset += snprintf(line->line_buffer + line->offset,
+ buffer_size - line->offset, "%c", 'c');
+ line->device_flag = 1;
+ line->color = DEVICE_COLOR;
+
+ } else if(S_ISFIFO(file_mode)) {
+ line->offset += snprintf(line->line_buffer + line->offset,
+ buffer_size - line->offset, "%c", 'p');
+ line->color = FIFO_COLOR;
+
+ } else if(S_ISSOCK(file_mode)) {
+ line->offset += snprintf(line->line_buffer + line->offset,
+ buffer_size - line->offset, "%c", 's');
+ line->color = SOCK_COLOR;
+ }
+}
+
+/*
+ * Build full output line (permissions, links, owner, size, date, name).
+ * Allocates buffer and assigns it to entry->info_line
+ */
+static void generate_information_line(const struct stat *file_info,
+ struct file_item *entry,
+ const char *link_target,
+ const struct print_options *opts)
+{
+ mode_t file_mode = file_info->st_mode;
+ struct permissions_mask permissions;
+ permissions.owner_rights = file_mode & S_IRWXU;
+ permissions.group_rights = file_mode & S_IRWXG;
+ permissions.other_rights = file_mode & S_IRWXO;
+
+ struct line_builder line;
+
+ line.line_buffer = malloc(buffer_size);
+ if (!line.line_buffer) {
+ perror("malloc");
+ exit(1);
+ }
+ line.color = "";
+ line.offset = 0;
+ line.device_flag = 0;
+ line.link_target = link_target;
+
+ append_file_type(&line,file_mode, &permissions);
+ append_permissions(&line, permissions.owner_rights, user_p, file_mode);
+ append_permissions(&line, permissions.group_rights, group_p, file_mode);
+ append_permissions(&line, permissions.other_rights, other_p, file_mode);
+ append_hard_links(&line, file_info);
+ append_owner_and_group(&line,file_info);
+ append_size_or_device(&line, file_info);
+ append_modification_date(&line, file_info);
+ append_file_name(&line, entry->filename, opts);
+ append_symlink_name(&line);
+
+ entry->info_line = line.line_buffer;
+}
+
+/* Print all prepared info lines */
+static void print_lines(const struct file_item_array *arr)
+{
+ int i;
+
+ for(i = 0; i < arr->length; ++i)
+ printf("%s", arr->array[i]->info_line);
+}
+
+/* Construct full path string: path + "/" + file_name */
+static void create_file_path(const char *file_name, const char *path,
+ char **file_path)
+{
+ size_t len = 0;
+
+ len = strlen(file_name) + 1 + strlen(path) + 1;
+ *file_path = malloc(len);
+ if(!*file_path) {
+ perror("malloc");
+ exit(1);
+ }
+ snprintf(*file_path, len, "%s/%s", path, file_name);
+}
+
+static int is_hidden_file(const char *file_name)
+{
+ return file_name[0] == '.';
+}
+
+static int should_skip_file(const struct stat *file_info,
+ const char *file_name)
+{
+ return
+ !strcmp(file_name, ".") || !strcmp(file_name, "..") ||
+ is_hidden_file(file_name);
+}
+
+/*
+ * Process single file: lstat, optional readlink(), create list item and
+ * line
+ */
+static void process_file(const char *file_path, const char *file_name,
+ struct file_item **list,
+ const struct print_options *opts)
+{
+ struct stat file_info;
+ int lstat_result;
+ ssize_t readlink_length = 0;
+ struct file_item *tmp = NULL;
+ char link_target[buffer_size];
+
+ lstat_result = lstat(file_path, &file_info);
+ if(lstat_result == -1) {
+ perror(file_path);
+ return;
+ }
+
+ if(S_ISLNK(file_info.st_mode)) {
+ readlink_length = readlink(file_path, link_target, buffer_size-1);
+
+ if(readlink_length == -1) {
+ perror(file_path);
+ return;
+ }
+
+ link_target[readlink_length] = '\0';
+ }
+ add_new_item(list, &tmp, file_name);
+ generate_information_line(&file_info, tmp,
+ S_ISLNK(file_info.st_mode) ? link_target : NULL,
+ opts);
+}
+
+/*
+ * Process directory: iterate files with readdir(), skip entries, compute total
+ * blocks, and build lines for each visible file
+ */
+static void process_directory(const char *path, struct file_item **list,
+ const struct print_options *opts)
+{
+ DIR *directory = NULL;
+ struct dirent *file = NULL;
+ struct stat file_info;
+ const char *file_name = NULL;
+ char *file_path = NULL;
+ blkcnt_t blocks_counter = 0;
+ int stat_result;
+
+ errno = 0;
+ directory = opendir(path);
+
+ if(directory == NULL) {
+ perror(path);
+ exit(1);
+ }
+
+ while((file = readdir(directory)) != NULL) {
+ file_name = file->d_name;
+
+ create_file_path(file_name, path, &file_path);
+ /* need to get st_blocks and filename */
+ stat_result = lstat(file_path, &file_info);
+
+ if(stat_result == -1 || should_skip_file(&file_info, file_name)) {
+ free(file_path);
+ continue;
+ }
+
+ blocks_counter += file_info.st_blocks;
+ process_file(file_path, file_name, list, opts);
+ free(file_path);
+ }
+ if(!file && errno) {
+ perror(path);
+ exit(1);
+ }
+ closedir(directory);
+ /* st_blocks are 512-byte units; display in 1024-byte units */
+ printf("total %ld\n", blocks_counter * block_size / display_block_size);
+}
+
+/* Prints the list of files from a sorted array with proper formatting */
+static void print_sorted_files(int argc, struct file_item **list,
+ struct file_item_array *array)
+{
+ /* if empty directory */
+ if(!*list)
+ return;
+ align_columns(*list);
+ copy_list_to_array(*list, array);
+ sort_array_by_names(array);
+ print_lines(array);
+ free_file_item_array(array, list);
+}
+
+/* Prints the directory name before listing its contents */
+static void print_directory_header(const char *dir_name,
+ int show_header)
+{
+ if(show_header)
+ printf("%s:\n", dir_name);
+}
+
+/* Prints a blank line to visually separate output between directories */
+static void print_directory_separator(const char **paths_arr, int argc,
+ size_t pos, int show_header,
+ enum arg_type mode)
+{
+ if(show_header &&
+ (mode == arg_file || !is_last_arr_element(paths_arr, argc, pos)))
+ {
+ putchar('\n');
+ }
+}
+
+/*
+ * Process either files or directories from paths_arr, align columns, sort,
+ * print lines and free allocated resources.
+ *
+ * For arg_file, the same string is used as both path and filename
+ * because user argument may include path. This differs from
+ * arg_directory, where path is combined with filenames from directory contents.
+ * print_directory_separator() receives 0, 0 for arg_file because
+ * last element check is only relevant for directories.
+ */
+static void process_args(const char **paths_arr, int argc,
+ enum arg_type mode, struct file_item **list,
+ struct file_item_array *array,
+ const struct print_options *opts)
+{
+ size_t i;
+
+ /*
+ * start from 0 index cause first element
+ * may contain "." if no user arguments
+ */
+ for(i = 0; i < argc; ++i) {
+ if(paths_arr[i] == 0)
+ continue;
+ if(mode == arg_directory) {
+ print_directory_header(paths_arr[i], opts->show_directory_header);
+ process_directory(paths_arr[i], list, opts);
+ print_sorted_files(argc, list, array);
+ print_directory_separator(paths_arr, argc, i,
+ opts->show_directory_header, mode);
+ } else if(mode == arg_file)
+ process_file(paths_arr[i], paths_arr[i], list, opts);
+ }
+ if(mode == arg_file && !is_empty_array(paths_arr, argc)) {
+ print_sorted_files(argc, list, array);
+ print_directory_separator(paths_arr, 0, 0, opts->show_directory_header,
+ mode);
+ }
+}
+
+/* Determines whether the directory name should be shown as a header */
+int needs_directory_header(struct arg_arrays *split, int argc)
+{
+ size_t i, counter = 0;
+
+ if(!is_empty_array(split->dirs, argc)) {
+ for(i = 1; i < argc; ++i)
+ if(split->files[i] || split->dirs[i])
+ ++counter;
+ }
+
+ return counter >= min_args_for_header;
+}
+
+int main(int argc, const char **argv)
+{
+ struct stat st;
+ struct file_item *list = NULL;
+ struct file_item_array array;
+ struct arg_arrays split;
+ struct print_options opt;
+ size_t i;
+
+ init_item_array(&array);
+ init_arg_arrays(&split, argc);
+ opt.output_to_tty = check_output_to_tty();
+ opt.show_directory_header = 0;
+
+ if(argc == 1) {
+ split.dirs[0] = ".";
+ process_args(split.dirs, argc, arg_directory, &list, &array, &opt);
+ } else {
+ for(i = 1; argv[i]; ++i) {
+ if(lstat(argv[i], &st) == -1) {
+ perror(argv[i]);
+ continue;
+ }
+ if(S_ISDIR(st.st_mode))
+ split.dirs[i] = argv[i];
+ else
+ split.files[i] = argv[i];
+ }
+ opt.show_directory_header = needs_directory_header(&split, argc);
+ process_args(split.files, argc, arg_file, &list, &array, &opt);
+ process_args(split.dirs, argc, arg_directory, &list, &array, &opt);
+ }
+ free_arg_arrays(&split);
+
+ return 0;
+}