/* SPDX-License-Identifier: Apache-2.0 */ /* Copyright 2025 Thorsten Töpper * * vim:ts=4:sw=4:expandtab */ #include #include #include #include #include #include #include #include #include #include #include #include #include "output.h" #include "data_management.h" #include "options.h" /* === DEFINITIONS === */ #define DM_OUT_FORMAT_MAX_LEN 80 #define box_cross '+' #define box_hsep '-' #define box_vsep '|' #define out_cross() fputc(box_cross, stdout) #define out_hsep() fputc(box_hsep, stdout) #define out_vsep() fputc(box_vsep, stdout) #define out_print_newline() fputc('\n', stdout) void out_print_fname(struct list_node *ptr, size_t longest_fname); void out_print_gid(struct list_node *ptr); void out_print_group_name(struct list_node *ptr); void out_print_permissions(struct list_node *ptr); void out_print_size(struct list_node *ptr); void out_print_symlink(struct list_node *ptr, size_t longest_fname); void out_print_time(time_t tv); void out_print_time_by_option(struct list_node *ptr); void out_print_type(struct list_node *ptr); void out_print_uid(struct list_node *ptr); void out_print_user_name(struct list_node *ptr); void out_print_format_string_header(const char *format, size_t longest_fname, size_t longest_symlink); size_t estimate_formatted_line_length(const char *format, size_t longest_fname, size_t longest_symlink); size_t get_longest_filename(struct list_head *list); size_t get_longest_symlink(struct list_head *list); char *global_sep_line = NULL; /* === IMPLEMENTATION === */ inline int fputc_all_cols(char c, FILE *fdout) { struct winsize terminal; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &terminal) == -1) { if (errno != 25) { LOGERR("ERROR: ioctl(STDOUT_FILENO, TIOCGWINSZ,...) failed: %s (errno %d)\n", strerror(errno), errno); } terminal.ws_col = 72; } DBGTRC("DEBUG: terminal.ws_col = %d / terminal.ws_row = %d\n", terminal.ws_col, terminal.ws_row); /* Somehow not always ws_col == 0, with optimization compiler flags and executed by watch. */ if (terminal.ws_col == 0) { terminal.ws_col = 72; } return fputc_width_x(c, terminal.ws_col, fdout); } /* Not the most performant way, but during output the time critical tasks are already done. */ inline int fputc_width_x(char c, size_t x, FILE *fdout) { size_t i=0; if (fdout == NULL) { LOGERR("ERROR: No output stream given.\n"); return EOF; } if ( ! isprint(c) ) { c = '?'; } for (i=0; i_fileno); return EOF; } } return 1; } /* basic 4 due to formatted output */ inline size_t get_longest_filename(struct list_head *list) { size_t rc = 4, tmp = 0; struct list_node *ptr = NULL; if (list == NULL) { return rc; } ptr = list->first; while (ptr != NULL) { tmp = strlen(ptr->fname); if (tmp>rc) { rc = tmp; } ptr = ptr->next; } return rc; } /* basic 7 due to formatted output */ inline size_t get_longest_symlink(struct list_head *list) { size_t rc = 7, tmp = 0; struct list_node *ptr = NULL; if (list == NULL) { return rc; } ptr = list->first; while(ptr != NULL) { if (ptr->symlink_target != NULL) { tmp = strlen(ptr->symlink_target); } else { tmp = 0; } if (tmp>rc) { rc = tmp; } ptr = ptr->next; } return rc; } /* === FORMATTING RELATED FUNCTIONS === */ inline void out_print_uid(struct list_node *ptr) { printf(" %4u ", ptr->ln_stat.st_gid); if (option_print_boxed_table) { out_vsep(); } } inline void out_print_gid(struct list_node *ptr) { printf(" %4u ", ptr->ln_stat.st_uid); if (option_print_boxed_table) { out_vsep(); } } inline void out_print_fname(struct list_node *ptr, size_t longest_fname) { size_t i = 0; printf(" %s ", ptr->fname); if (option_print_boxed_table) { for (i=0; i<(longest_fname - strlen(ptr->fname)); i++) { fputc(' ', stdout); } out_vsep(); } } inline void out_print_symlink(struct list_node *ptr, size_t longest_symlink) { size_t i = 0, sll = 0; if (ptr->symlink_target != NULL) { sll = strlen(ptr->symlink_target); printf(" %s ", ptr->symlink_target); } else { printf(" "); } if (option_print_boxed_table) { for (i=0; i<(longest_symlink - sll); i++) { fputc(' ', stdout); } out_vsep(); } } /* Be aware: getting the name via uid takes a lot of time depending on the * systems setup. local, NIS, LDAP ... * Caching happens outside the executable, minimal here for skipping funcion calls. */ inline void out_print_group_name(struct list_node *ptr) { struct group *group = NULL; static gid_t prev_gid = UINT32_MAX; /* TODO: move to heap with sapconf(_SC_LOGIN_NAME_MAX) length */ static char prev_group_name[256] = ""; size_t group_name_length = 0; /* For the unlikely case of multi threading this program: lock required here... */ if (prev_gid == ptr->ln_stat.st_gid) { if (prev_group_name[0] == '\0') { /* first call of function and GID UINT32_MAX ? */ if ((group = getgrgid(prev_gid)) == NULL) { LOGERR("ERROR: Failed to resolve information for gid %u: %s (errno %d)\n", prev_gid, strerror(errno), errno); return; } group_name_length = strlen(group->gr_name); group_name_length = (group_name_length > 63) ? 63 : group_name_length; explicit_bzero(prev_group_name, 64); memcpy(prev_group_name, group->gr_name, group_name_length); } printf(" %-10s ", prev_group_name); return; } prev_gid = ptr->ln_stat.st_gid; if ((group = getgrgid(prev_gid)) == NULL) { LOGERR("ERROR: Failed to resolve information for gid %u: %s (errno %d)\n", prev_gid, strerror(errno), errno); return; } group_name_length = strlen(group->gr_name); group_name_length = (group_name_length > 63) ? 63 : group_name_length; explicit_bzero(prev_group_name, 64); memcpy(prev_group_name, group->gr_name, group_name_length); printf(" %-10s ", prev_group_name); if (option_print_boxed_table) { out_vsep(); } } /* Reminder: resolving may take some time even with cache in libraries, * minimal cache here to prevent function call */ inline void out_print_user_name(struct list_node *ptr) { struct passwd *pwd = NULL; static uid_t prev_uid = UINT32_MAX; /* TODO: move to heap with sapconf(_SC_LOGIN_NAME_MAX) length */ static char prev_user_name[256] = ""; size_t user_name_length = 0; /* For the unlikely case of multi threading this program: lock required here... */ if (prev_uid == ptr->ln_stat.st_uid) { if (prev_user_name[0] == '\0') { /* first call of function and UID UINT32_MAX ? */ if ((pwd = getpwuid(prev_uid)) == NULL) { LOGERR("ERROR: Failed to resolve information for uid %u: %s (errno %d)\n", prev_uid, strerror(errno), errno); return; } user_name_length = strlen(pwd->pw_name); user_name_length = (user_name_length > 63) ? 63 : user_name_length; explicit_bzero(prev_user_name, 64); memcpy(prev_user_name, pwd->pw_name, user_name_length); } printf(" %-10s ", prev_user_name); return; } prev_uid = ptr->ln_stat.st_uid; if ((pwd = getpwuid(prev_uid)) == NULL) { LOGERR("ERROR: Failed to resolve information for uid %u: %s (errno %d)\n", prev_uid, strerror(errno), errno); return; } user_name_length = strlen(pwd->pw_name); user_name_length = (user_name_length > 63) ? 63 : user_name_length; explicit_bzero(prev_user_name, 64); memcpy(prev_user_name, pwd->pw_name, user_name_length); printf(" %-10s ", prev_user_name); if (option_print_boxed_table) { out_vsep(); } } inline void out_print_size(struct list_node *ptr) { char unit[4] = ""; off_t tmp = ptr->ln_stat.st_size; if (tmp >= 1024) { tmp /= 1024; unit[0] = 'K'; unit[1] = 'i'; unit[2] = 'B'; /* > 10 GiB as MiB */ if (tmp > (10*1024*1024)) { tmp /= 1024; unit[0] = 'M'; } } printf(" %8ld %3s ", tmp, unit); if (option_print_boxed_table) { out_vsep(); } } inline void out_print_time(time_t tv) { struct tm *tm = NULL; char timestamp[128]; tm = localtime(&tv); if (option_timestamp_long) { strftime(timestamp, sizeof(timestamp), "%Y%m%d %H:%M:%S %Z", tm); printf(" %-22s ", timestamp); } else { strftime(timestamp, sizeof(timestamp), "%H:%M:%S", tm); printf(" %-8s ", timestamp); } if (option_print_boxed_table) { out_vsep(); } } inline void out_print_time_by_option(struct list_node *ptr) { time_t time_value = 0; switch (option_time_field) { case 'm': time_value = ptr->ln_stat.st_mtim.tv_sec; break; case 'a': time_value = ptr->ln_stat.st_atim.tv_sec; break; case 'c': time_value = ptr->ln_stat.st_ctim.tv_sec; break; default: LOGERR("WARNING: option_time_field has invalid value %c 0x%02X - going by default m\n", option_time_field, option_time_field); option_time_field = 'm'; time_value = ptr->ln_stat.st_mtim.tv_sec; break; }; out_print_time(time_value); } inline void out_print_type(struct list_node *ptr) { char entry[16] = ""; /* regular file, directory and symlinks are most common */ if (S_ISREG(ptr->ln_stat.st_mode)) { sprintf(entry, "regular file"); } else if (S_ISDIR(ptr->ln_stat.st_mode)) { sprintf(entry, "directory"); } else if (S_ISLNK(ptr->ln_stat.st_mode)) { sprintf(entry, "symlink"); } else if (S_ISBLK(ptr->ln_stat.st_mode)) { sprintf(entry, "block"); } else if (S_ISCHR(ptr->ln_stat.st_mode)) { sprintf(entry, "char"); } else if (S_ISFIFO(ptr->ln_stat.st_mode)) { sprintf(entry, "fifo"); } else if (S_ISSOCK(ptr->ln_stat.st_mode)) { sprintf(entry, "socket"); /* The following macros operate on the complete struct stat */ } else if (S_TYPEISMQ(&(ptr->ln_stat))) { sprintf(entry, "message queue"); } else if (S_TYPEISSEM(&(ptr->ln_stat))) { sprintf(entry, "semaphore"); } else if (S_TYPEISSHM(&(ptr->ln_stat))) { sprintf(entry, "shared memory"); #if 0 /* Although mentioned in the POSIX man page, not part of glibc */ } else if (S_TYPEISTMO(&(ptr->ln_stat))) { sprintf(entry, "memory object"); #endif } else { sprintf(entry, "unidentified"); } /* The ones longer than 12 characters are rare. */ printf(" %-12s ", entry); if (option_print_boxed_table) { out_vsep(); } } inline void out_print_permissions(struct list_node *ptr) { mode_t mode = 0; /* User permissions */ if (ptr->ln_stat.st_mode & S_IXUSR) { mode |= S_IXUSR; } if (ptr->ln_stat.st_mode & S_IWUSR) { mode |= S_IWUSR; } if (ptr->ln_stat.st_mode & S_IRUSR) { mode |= S_IRUSR; } /* Group permissions */ if (ptr->ln_stat.st_mode & S_IXGRP) { mode |= S_IXGRP; } if (ptr->ln_stat.st_mode & S_IWGRP) { mode |= S_IWGRP; } if (ptr->ln_stat.st_mode & S_IRGRP) { mode |= S_IRGRP; } /* Other permissions */ if (ptr->ln_stat.st_mode & S_IXOTH) { mode |= S_IXOTH; } if (ptr->ln_stat.st_mode & S_IWOTH) { mode |= S_IWOTH; } if (ptr->ln_stat.st_mode & S_IROTH) { mode |= S_IROTH; } /* special bits */ if (ptr->ln_stat.st_mode & S_ISVTX) { mode |= S_ISVTX; } if (ptr->ln_stat.st_mode & S_ISGID) { mode |= S_ISGID; } if (ptr->ln_stat.st_mode & S_ISUID) { mode |= S_ISUID; } printf(" %4o ", mode); if (option_print_boxed_table) { out_vsep(); } } /* === END OF FORMATTING FUNCTIONS === */ /* Interpret the format string and based on it generate a size which will be * able to store everything including visual additions. Use strlen later. */ inline size_t estimate_formatted_line_length(const char *format, size_t longest_fname, size_t longest_symlink) { size_t rc = 0, i=0, format_len=0; static int login_name_max = -1; if (login_name_max < 0) { /* TODO: Check whether all UNIX likes use this for user and group name*/ if ((login_name_max=(int)sysconf(_SC_LOGIN_NAME_MAX)) < 0) { LOGERR("ERROR: sysconf(_SC_LOGIN_NAME_MAX) failed: %s (errno %d)\n", strerror(errno), errno); return 0; } DBGTRC("DEBUG: sysconf(_SC_LOGIN_NAME_MAX) = %d\n", login_name_max); login_name_max += 4; /* formatting additions */ } format_len = strnlen(format, DM_OUT_FORMAT_MAX_LEN); for (i=0; i10 in format*/ }; } DBGTRC("DEBUG: format string '%s' estimated with %lu bytes\n", format, rc); return rc; } inline void out_print_format_string_header(const char *format, size_t longest_fname, size_t longest_symlink) { size_t i=0, j=0, k=0, format_len=0, est_length=0, tmp = 0; char *header = NULL, *sep_line = NULL; char *pos = NULL; if (format == NULL || format[0] == '\0' || longest_fname == 0) return; DBGTRC("DEBUG: format string: \"%s\"\n", format); format_len = strnlen(format, DM_OUT_FORMAT_MAX_LEN); est_length = estimate_formatted_line_length(format, longest_fname, longest_symlink); if ((header=calloc(est_length+1, sizeof(char))) == NULL) { LOGERR("ERROR: Failed to allocate memory for header\n"); return; } pos = header; if (option_print_boxed_table) { if ((sep_line=calloc(est_length+1, sizeof(char))) == NULL) { free(header); LOGERR("ERROR: Failed to allocate memory for sep line\n"); return; } if (global_sep_line != NULL) { free(global_sep_line); } pos[0] = box_vsep; pos++; sep_line[j++] = box_cross; } #define LOCAL_EXTEND_SEP_LINE() { \ if (option_print_boxed_table) { \ tmp = strlen(pos); \ for (k=0; kfirst == NULL) { LOGERR("ERROR: No data to print.\n"); return; } if (option_sort_reverse_order) { lh = create_list_sort_reversed(list); /* be tolerant */ if (lh == NULL) { lh = list; } } /* Note: the maximum length needs to be adjusted if the strings * shall contain more complicated settings */ format_len = strnlen(format, DM_OUT_FORMAT_MAX_LEN); DBGTRC("DEBUG: format_len = %lu\n", format_len); longest_fname = get_longest_filename(list); if (option_resolve_symlinks) { longest_symlink = get_longest_symlink(list); } if (option_print_header) { out_print_format_string_header(format, longest_fname, longest_symlink); } ptr = lh->first; while (ptr != NULL) { if (option_print_boxed_table) { out_vsep(); } for (i=0; i < format_len; i++) { switch (format[i]) { case 'n': out_print_fname(ptr, longest_fname); break; case 'l': if (option_resolve_symlinks) { out_print_symlink(ptr, longest_symlink); } break; case 's': out_print_size(ptr); break; case 't': out_print_time_by_option(ptr); break; case 'A': out_print_time(ptr->ln_stat.st_atim.tv_sec); break; case 'C': out_print_time(ptr->ln_stat.st_ctim.tv_sec); break; case 'M': out_print_time(ptr->ln_stat.st_mtim.tv_sec); break; case 'T': out_print_type(ptr); break; case 'u': out_print_uid(ptr); break; case 'g': out_print_gid(ptr); break; case 'U': out_print_user_name(ptr); break; case 'G': out_print_group_name(ptr); break; case 'p': out_print_permissions(ptr); break; case ' ': /* just ignore this without warning */ break; default: LOGERR("WARNING: Invalid character 0x%02X ignoring it\n", format[i]); break; }; } out_print_newline(); total_size += (ptr->ln_stat.st_size>0) ? (unsigned long int)ptr->ln_stat.st_size : 0; ptr = ptr->next; } if (option_print_boxed_table && global_sep_line != NULL) { fputs(global_sep_line, stdout); } else { fputc_all_cols('=', stdout); } out_print_newline(); printf("\nTotal size: %lu %s\n", ((total_size>1024) ? total_size/1024 : total_size), ((total_size >= 1024) ? "KiB" : "")); if (lh != list) destroy_list(lh); }