1 /* GNU's pinky.
2    Copyright (C) 1992-2023 Free Software Foundation, Inc.
3 
4    This program is free software: you can redistribute it and/or modify
5    it under the terms of the GNU General Public License as published by
6    the Free Software Foundation, either version 3 of the License, or
7    (at your option) any later version.
8 
9    This program is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY; without even the implied warranty of
11    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12    GNU General Public License for more details.
13 
14    You should have received a copy of the GNU General Public License
15    along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
16 
17 /* Created by hacking who.c by Kaveh Ghazi ghazi@caip.rutgers.edu */
18 
19 #include <config.h>
20 #include <getopt.h>
21 #include <pwd.h>
22 #include <stdckdint.h>
23 #include <stdio.h>
24 
25 #include <sys/types.h>
26 #include "system.h"
27 
28 #include "canon-host.h"
29 #include "hard-locale.h"
30 #include "readutmp.h"
31 
32 /* The official name of this program (e.g., no 'g' prefix).  */
33 #define PROGRAM_NAME "pinky"
34 
35 #define AUTHORS \
36   proper_name ("Joseph Arceneaux"), \
37   proper_name ("David MacKenzie"), \
38   proper_name ("Kaveh Ghazi")
39 
40 /* If true, display the hours:minutes since each user has touched
41    the keyboard, or blank if within the last minute, or days followed
42    by a 'd' if not within the last day. */
43 static bool include_idle = true;
44 
45 /* If true, display a line at the top describing each field. */
46 static bool include_heading = true;
47 
48 /* if true, display the user's full name from pw_gecos. */
49 static bool include_fullname = true;
50 
51 /* if true, display the user's ~/.project file when doing long format. */
52 static bool include_project = true;
53 
54 /* if true, display the user's ~/.plan file when doing long format. */
55 static bool include_plan = true;
56 
57 /* if true, display the user's home directory and shell
58    when doing long format. */
59 static bool include_home_and_shell = true;
60 
61 /* if true, use the "short" output format. */
62 static bool do_short_format = true;
63 
64 /* if true, display the ut_host field. */
65 #if HAVE_STRUCT_XTMP_UT_HOST
66 static bool include_where = true;
67 #endif
68 
69 /* The strftime format to use for login times, and its expected
70    output width.  */
71 static char const *time_format;
72 static int time_format_width;
73 
74 static struct option const longopts[] =
75 {
76   {GETOPT_HELP_OPTION_DECL},
77   {GETOPT_VERSION_OPTION_DECL},
78   {nullptr, 0, nullptr, 0}
79 };
80 
81 /* Count and return the number of ampersands in STR.  */
82 
83 ATTRIBUTE_PURE
84 static size_t
count_ampersands(char const * str)85 count_ampersands (char const *str)
86 {
87   size_t count = 0;
88   do
89     {
90       if (*str == '&')
91         count++;
92     } while (*str++);
93   return count;
94 }
95 
96 /* Create a string (via xmalloc) which contains a full name by substituting
97    for each ampersand in GECOS_NAME the USER_NAME string with its first
98    character capitalized.  The caller must ensure that GECOS_NAME contains
99    no ','s.  The caller also is responsible for free'ing the return value of
100    this function.  */
101 
102 static char *
create_fullname(char const * gecos_name,char const * user_name)103 create_fullname (char const *gecos_name, char const *user_name)
104 {
105   size_t rsize = strlen (gecos_name) + 1;
106   char *result;
107   char *r;
108   size_t ampersands = count_ampersands (gecos_name);
109 
110   if (ampersands != 0)
111     {
112       size_t ulen = strlen (user_name);
113       size_t product;
114       if (ckd_mul (&product, ulen, ampersands - 1)
115           || ckd_add (&rsize, rsize, product))
116         xalloc_die ();
117     }
118 
119   r = result = xmalloc (rsize);
120 
121   while (*gecos_name)
122     {
123       if (*gecos_name == '&')
124         {
125           char const *uname = user_name;
126           if (islower (to_uchar (*uname)))
127             *r++ = toupper (to_uchar (*uname++));
128           while (*uname)
129             *r++ = *uname++;
130         }
131       else
132         {
133           *r++ = *gecos_name;
134         }
135 
136       gecos_name++;
137     }
138   *r = 0;
139 
140   return result;
141 }
142 
143 /* Return a string representing the time between WHEN and the time
144    that this function is first run. */
145 
146 static char const *
idle_string(time_t when)147 idle_string (time_t when)
148 {
149   static time_t now = 0;
150   static char buf[INT_STRLEN_BOUND (intmax_t) + sizeof "d"];
151   time_t seconds_idle;
152 
153   if (now == 0)
154     time (&now);
155 
156   seconds_idle = now - when;
157   if (seconds_idle < 60)	/* One minute. */
158     return "     ";
159   if (seconds_idle < (24 * 60 * 60))	/* One day. */
160     {
161       int hours = seconds_idle / (60 * 60);
162       int minutes = (seconds_idle % (60 * 60)) / 60;
163       sprintf (buf, "%02d:%02d", hours, minutes);
164     }
165   else
166     {
167       intmax_t days = seconds_idle / (24 * 60 * 60);
168       sprintf (buf, "%jdd", days);
169     }
170   return buf;
171 }
172 
173 /* Return a time string.  */
174 static char const *
time_string(struct gl_utmp const * utmp_ent)175 time_string (struct gl_utmp const *utmp_ent)
176 {
177   static char buf[INT_STRLEN_BOUND (intmax_t) + sizeof "-%m-%d %H:%M"];
178   struct tm *tmp = localtime (&utmp_ent->ut_ts.tv_sec);
179 
180   if (tmp)
181     {
182       strftime (buf, sizeof buf, time_format, tmp);
183       return buf;
184     }
185   else
186     return timetostr (utmp_ent->ut_ts.tv_sec, buf);
187 }
188 
189 /* Display a line of information about UTMP_ENT. */
190 
191 static void
print_entry(struct gl_utmp const * utmp_ent)192 print_entry (struct gl_utmp const *utmp_ent)
193 {
194   struct stat stats;
195   time_t last_change;
196   char mesg;
197 
198   /* If ut_line contains a space, the device name starts after the space.  */
199   char *line = utmp_ent->ut_line;
200   char *space = strchr (line, ' ');
201   line = space ? space + 1 : line;
202 
203   int dirfd;
204   if (IS_ABSOLUTE_FILE_NAME (line))
205     dirfd = AT_FDCWD;
206   else
207     {
208       static int dev_dirfd;
209       if (!dev_dirfd)
210         {
211           dev_dirfd = open ("/dev", O_PATHSEARCH | O_DIRECTORY);
212           if (dev_dirfd < 0)
213             dev_dirfd = AT_FDCWD - 1;
214         }
215       dirfd = dev_dirfd;
216     }
217 
218   if (AT_FDCWD <= dirfd && fstatat (dirfd, line, &stats, 0) == 0)
219     {
220       mesg = (stats.st_mode & S_IWGRP) ? ' ' : '*';
221       last_change = stats.st_atime;
222     }
223   else
224     {
225       mesg = '?';
226       last_change = 0;
227     }
228 
229   char *ut_user = utmp_ent->ut_user;
230   if (strnlen (ut_user, 8) < 8)
231     printf ("%-8s", ut_user);
232   else
233     fputs (ut_user, stdout);
234 
235   if (include_fullname)
236     {
237       struct passwd *pw = getpwnam (ut_user);
238       if (pw == nullptr)
239         /* TRANSLATORS: Real name is unknown; at most 19 characters. */
240         printf (" %19s", _("        ???"));
241       else
242         {
243           char *const comma = strchr (pw->pw_gecos, ',');
244           char *result;
245 
246           if (comma)
247             *comma = '\0';
248 
249           result = create_fullname (pw->pw_gecos, pw->pw_name);
250           printf (" %-19.19s", result);
251           free (result);
252         }
253     }
254 
255   fputc (' ', stdout);
256   fputc (mesg, stdout);
257   if (strnlen (utmp_ent->ut_line, 8) < 8)
258     printf ("%-8s", utmp_ent->ut_line);
259   else
260     fputs (utmp_ent->ut_line, stdout);
261 
262   if (include_idle)
263     {
264       if (last_change)
265         printf (" %-6s", idle_string (last_change));
266       else
267         /* TRANSLATORS: Idle time is unknown; at most 5 characters. */
268         printf (" %-6s", _("?????"));
269     }
270 
271   printf (" %s", time_string (utmp_ent));
272 
273 #ifdef HAVE_STRUCT_XTMP_UT_HOST
274   if (include_where && utmp_ent->ut_host[0])
275     {
276       char *host = nullptr;
277       char *display = nullptr;
278       char *ut_host = utmp_ent->ut_host;
279 
280       /* Look for an X display.  */
281       display = strchr (ut_host, ':');
282       if (display)
283         *display++ = '\0';
284 
285       if (*ut_host)
286         /* See if we can canonicalize it.  */
287         host = canon_host (ut_host);
288       if ( ! host)
289         host = ut_host;
290 
291       fputc (' ', stdout);
292       fputs (host, stdout);
293       if (display)
294         {
295           fputc (':', stdout);
296           fputs (display, stdout);
297         }
298 
299       if (host != ut_host)
300         free (host);
301     }
302 #endif
303 
304   putchar ('\n');
305 }
306 
307 /* Display a verbose line of information about UTMP_ENT. */
308 
309 static void
print_long_entry(const char name[])310 print_long_entry (const char name[])
311 {
312   struct passwd *pw;
313 
314   pw = getpwnam (name);
315 
316   printf (_("Login name: "));
317   printf ("%-28s", name);
318 
319   printf (_("In real life: "));
320   if (pw == nullptr)
321     {
322       /* TRANSLATORS: Real name is unknown; no hard limit. */
323       printf (" %s", _("???\n"));
324       return;
325     }
326   else
327     {
328       char *const comma = strchr (pw->pw_gecos, ',');
329       char *result;
330 
331       if (comma)
332         *comma = '\0';
333 
334       result = create_fullname (pw->pw_gecos, pw->pw_name);
335       printf (" %s", result);
336       free (result);
337     }
338 
339   putchar ('\n');
340 
341   if (include_home_and_shell)
342     {
343       printf (_("Directory: "));
344       printf ("%-29s", pw->pw_dir);
345       printf (_("Shell: "));
346       printf (" %s", pw->pw_shell);
347       putchar ('\n');
348     }
349 
350   if (include_project)
351     {
352       FILE *stream;
353       char buf[1024];
354       char const *const baseproject = "/.project";
355       char *const project =
356         xmalloc (strlen (pw->pw_dir) + strlen (baseproject) + 1);
357       stpcpy (stpcpy (project, pw->pw_dir), baseproject);
358 
359       stream = fopen (project, "r");
360       if (stream)
361         {
362           size_t bytes;
363 
364           printf (_("Project: "));
365 
366           while ((bytes = fread (buf, 1, sizeof (buf), stream)) > 0)
367             fwrite (buf, 1, bytes, stdout);
368           fclose (stream);
369         }
370 
371       free (project);
372     }
373 
374   if (include_plan)
375     {
376       FILE *stream;
377       char buf[1024];
378       char const *const baseplan = "/.plan";
379       char *const plan =
380         xmalloc (strlen (pw->pw_dir) + strlen (baseplan) + 1);
381       stpcpy (stpcpy (plan, pw->pw_dir), baseplan);
382 
383       stream = fopen (plan, "r");
384       if (stream)
385         {
386           size_t bytes;
387 
388           printf (_("Plan:\n"));
389 
390           while ((bytes = fread (buf, 1, sizeof (buf), stream)) > 0)
391             fwrite (buf, 1, bytes, stdout);
392           fclose (stream);
393         }
394 
395       free (plan);
396     }
397 
398   putchar ('\n');
399 }
400 
401 /* Print the username of each valid entry and the number of valid entries
402    in UTMP_BUF, which should have N elements. */
403 
404 static void
print_heading(void)405 print_heading (void)
406 {
407   printf ("%-8s", _("Login"));
408   if (include_fullname)
409     printf (" %-19s", _("Name"));
410   printf (" %-9s", _(" TTY"));
411   if (include_idle)
412     printf (" %-6s", _("Idle"));
413   printf (" %-*s", time_format_width, _("When"));
414 #ifdef HAVE_STRUCT_XTMP_UT_HOST
415   if (include_where)
416     printf (" %s", _("Where"));
417 #endif
418   putchar ('\n');
419 }
420 
421 /* Display UTMP_BUF, which should have N entries. */
422 
423 static void
scan_entries(idx_t n,struct gl_utmp const * utmp_buf,const int argc_names,char * const argv_names[])424 scan_entries (idx_t n, struct gl_utmp const *utmp_buf,
425               const int argc_names, char *const argv_names[])
426 {
427   if (hard_locale (LC_TIME))
428     {
429       time_format = "%Y-%m-%d %H:%M";
430       time_format_width = 4 + 1 + 2 + 1 + 2 + 1 + 2 + 1 + 2;
431     }
432   else
433     {
434       time_format = "%b %e %H:%M";
435       time_format_width = 3 + 1 + 2 + 1 + 2 + 1 + 2;
436     }
437 
438   if (include_heading)
439     print_heading ();
440 
441   while (n--)
442     {
443       if (IS_USER_PROCESS (utmp_buf))
444         {
445           if (argc_names)
446             {
447               for (int i = 0; i < argc_names; i++)
448                 if (STREQ (utmp_buf->ut_user, argv_names[i]))
449                   {
450                     print_entry (utmp_buf);
451                     break;
452                   }
453             }
454           else
455             print_entry (utmp_buf);
456         }
457       utmp_buf++;
458     }
459 }
460 
461 /* Display a list of who is on the system, according to utmp file FILENAME. */
462 
463 static void
short_pinky(char const * filename,const int argc_names,char * const argv_names[])464 short_pinky (char const *filename,
465              const int argc_names, char *const argv_names[])
466 {
467   idx_t n_users;
468   struct gl_utmp *utmp_buf;
469   if (read_utmp (filename, &n_users, &utmp_buf, READ_UTMP_USER_PROCESS) != 0)
470     error (EXIT_FAILURE, errno, "%s", quotef (filename));
471 
472   scan_entries (n_users, utmp_buf, argc_names, argv_names);
473   exit (EXIT_SUCCESS);
474 }
475 
476 static void
long_pinky(const int argc_names,char * const argv_names[])477 long_pinky (const int argc_names, char *const argv_names[])
478 {
479   for (int i = 0; i < argc_names; i++)
480     print_long_entry (argv_names[i]);
481 }
482 
483 void
usage(int status)484 usage (int status)
485 {
486   if (status != EXIT_SUCCESS)
487     emit_try_help ();
488   else
489     {
490       printf (_("Usage: %s [OPTION]... [USER]...\n"), program_name);
491       fputs (_("\
492 \n\
493   -l              produce long format output for the specified USERs\n\
494   -b              omit the user's home directory and shell in long format\n\
495   -h              omit the user's project file in long format\n\
496   -p              omit the user's plan file in long format\n\
497   -s              do short format output, this is the default\n\
498 "), stdout);
499       fputs (_("\
500   -f              omit the line of column headings in short format\n\
501   -w              omit the user's full name in short format\n\
502   -i              omit the user's full name and remote host in short format\n\
503   -q              omit the user's full name, remote host and idle time\n\
504                   in short format\n\
505 "), stdout);
506       fputs (HELP_OPTION_DESCRIPTION, stdout);
507       fputs (VERSION_OPTION_DESCRIPTION, stdout);
508       printf (_("\
509 \n\
510 A lightweight 'finger' program;  print user information.\n\
511 The utmp file will be %s.\n\
512 "), UTMP_FILE);
513       emit_ancillary_info (PROGRAM_NAME);
514     }
515   exit (status);
516 }
517 
518 int
main(int argc,char ** argv)519 main (int argc, char **argv)
520 {
521   int optc;
522   int n_users;
523 
524   initialize_main (&argc, &argv);
525   set_program_name (argv[0]);
526   setlocale (LC_ALL, "");
527   bindtextdomain (PACKAGE, LOCALEDIR);
528   textdomain (PACKAGE);
529 
530   atexit (close_stdout);
531 
532   while ((optc = getopt_long (argc, argv, "sfwiqbhlp", longopts, nullptr))
533          != -1)
534     {
535       switch (optc)
536         {
537         case 's':
538           do_short_format = true;
539           break;
540 
541         case 'l':
542           do_short_format = false;
543           break;
544 
545         case 'f':
546           include_heading = false;
547           break;
548 
549         case 'w':
550           include_fullname = false;
551           break;
552 
553         case 'i':
554           include_fullname = false;
555 #ifdef HAVE_STRUCT_XTMP_UT_HOST
556           include_where = false;
557 #endif
558           break;
559 
560         case 'q':
561           include_fullname = false;
562 #ifdef HAVE_STRUCT_XTMP_UT_HOST
563           include_where = false;
564 #endif
565           include_idle = false;
566           break;
567 
568         case 'h':
569           include_project = false;
570           break;
571 
572         case 'p':
573           include_plan = false;
574           break;
575 
576         case 'b':
577           include_home_and_shell = false;
578           break;
579 
580         case_GETOPT_HELP_CHAR;
581 
582         case_GETOPT_VERSION_CHAR (PROGRAM_NAME, AUTHORS);
583 
584         default:
585           usage (EXIT_FAILURE);
586         }
587     }
588 
589   n_users = argc - optind;
590 
591   if (!do_short_format && n_users == 0)
592     {
593       error (0, 0, _("no username specified; at least one must be\
594  specified when using -l"));
595       usage (EXIT_FAILURE);
596     }
597 
598   if (do_short_format)
599     short_pinky (UTMP_FILE, n_users, argv + optind);
600   else
601     long_pinky (n_users, argv + optind);
602 
603   return EXIT_SUCCESS;
604 }
605