From 6b7b0ea022ad18b30622acd5e1354ff64cf14d86 Mon Sep 17 00:00:00 2001 From: scratko Date: Mon, 8 Sep 2025 01:44:13 +0300 Subject: Initial commit --- ls_imitation.c | 620 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 620 insertions(+) create mode 100644 ls_imitation.c (limited to 'ls_imitation.c') 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* 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; +} -- cgit v1.2.3