/* * 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; }