Merge branch '4292_autocomplete_slashes'
[midnight-commander.git] / lib / vfs / parse_ls_vga.c
blobdcb82be7cc1be9cc6d753386a49926ffc7760d5f
1 /*
2 Routines for parsing output from the 'ls' command.
4 Copyright (C) 1988-2024
5 Free Software Foundation, Inc.
7 Copyright (C) 1995, 1996 Miguel de Icaza
9 Written by:
10 Miguel de Icaza, 1995, 1996
11 Slava Zanko <slavazanko@gmail.com>, 2011
13 This file is part of the Midnight Commander.
15 The Midnight Commander is free software: you can redistribute it
16 and/or modify it under the terms of the GNU General Public License as
17 published by the Free Software Foundation, either version 3 of the License,
18 or (at your option) any later version.
20 The Midnight Commander is distributed in the hope that it will be useful,
21 but WITHOUT ANY WARRANTY; without even the implied warranty of
22 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 GNU General Public License for more details.
25 You should have received a copy of the GNU General Public License
26 along with this program. If not, see <http://www.gnu.org/licenses/>.
29 /**
30 * \file
31 * \brief Source: Utilities for VFS modules
32 * \author Miguel de Icaza
33 * \date 1995, 1996
36 #include <config.h>
38 #include <ctype.h>
39 #include <stdio.h>
40 #include <stdlib.h>
42 #include "lib/global.h"
43 #include "lib/unixcompat.h" /* makedev */
44 #include "lib/widget.h" /* message() */
46 #include "utilvfs.h"
48 /*** global variables ****************************************************************************/
50 /*** file scope macro definitions ****************************************************************/
52 /* Parsing code is used by ftpfs, shell and extfs */
53 #define MAXCOLS 30
55 /*** file scope type declarations ****************************************************************/
57 /*** forward declarations (file scope functions) *************************************************/
59 /*** file scope variables ************************************************************************/
61 static char *columns[MAXCOLS]; /* Points to the string in column n */
62 static int column_ptr[MAXCOLS]; /* Index from 0 to the starting positions of the columns */
63 static size_t vfs_parce_ls_final_num_spaces = 0;
65 /* --------------------------------------------------------------------------------------------- */
66 /*** file scope functions ************************************************************************/
67 /* --------------------------------------------------------------------------------------------- */
69 static gboolean
70 is_num (int idx)
72 char *column = columns[idx];
74 return (column != NULL && isdigit (column[0]));
77 /* --------------------------------------------------------------------------------------------- */
78 /* Return TRUE for MM-DD-YY and MM-DD-YYYY */
80 static gboolean
81 is_dos_date (const char *str)
83 size_t len;
85 if (str == NULL)
86 return FALSE;
88 len = strlen (str);
89 if (len != 8 && len != 10)
90 return FALSE;
92 if (str[2] != str[5])
93 return FALSE;
95 return (strchr ("\\-/", (int) str[2]) != NULL);
98 /* --------------------------------------------------------------------------------------------- */
100 static gboolean
101 is_week (const char *str, struct tm *tim)
103 static const char *week = "SunMonTueWedThuFriSat";
104 const char *pos;
106 if (str == NULL)
107 return FALSE;
109 pos = strstr (week, str);
110 if (pos == NULL)
111 return FALSE;
113 if (tim != NULL)
114 tim->tm_wday = (pos - week) / 3;
116 return TRUE;
119 /* --------------------------------------------------------------------------------------------- */
121 * Check for possible locale's abbreviated month name (Jan..Dec).
122 * Any 3 bytes long string without digit, control and punctuation characters.
123 * isalpha() is locale specific, so it cannot be used if current
124 * locale is "C" and ftp server use Cyrillic.
125 * NB: It is assumed there are no whitespaces in month.
127 static gboolean
128 is_localized_month (const char *month)
130 int i;
132 if (month == NULL)
133 return FALSE;
135 for (i = 0;
136 i < 3 && *month != '\0' && !isdigit ((unsigned char) *month)
137 && !iscntrl ((unsigned char) *month) && !ispunct ((unsigned char) *month); i++, month++)
140 return (i == 3 && *month == '\0');
143 /* --------------------------------------------------------------------------------------------- */
145 static gboolean
146 is_time (const char *str, struct tm *tim)
148 const char *p, *p2;
150 if (str == NULL)
151 return FALSE;
153 p = strchr (str, ':');
154 if (p == NULL)
155 return FALSE;
157 p2 = strrchr (str, ':');
158 if (p2 == NULL)
159 return FALSE;
161 if (p != p2)
163 if (sscanf (str, "%2d:%2d:%2d", &tim->tm_hour, &tim->tm_min, &tim->tm_sec) != 3)
164 return FALSE;
166 else
168 if (sscanf (str, "%2d:%2d", &tim->tm_hour, &tim->tm_min) != 2)
169 return FALSE;
172 return TRUE;
175 /* --------------------------------------------------------------------------------------------- */
177 static gboolean
178 is_year (char *str, struct tm *tim)
180 long year;
182 if (str == NULL)
183 return FALSE;
185 if (strchr (str, ':') != NULL)
186 return FALSE;
188 if (strlen (str) != 4)
189 return FALSE;
191 /* cppcheck-suppress invalidscanf */
192 if (sscanf (str, "%ld", &year) != 1)
193 return FALSE;
195 if (year < 1900 || year > 3000)
196 return FALSE;
198 tim->tm_year = (int) (year - 1900);
200 return TRUE;
203 /* --------------------------------------------------------------------------------------------- */
204 /*** public functions ****************************************************************************/
205 /* --------------------------------------------------------------------------------------------- */
207 gboolean
208 vfs_parse_filetype (const char *s, size_t * ret_skipped, mode_t * ret_type)
210 mode_t type;
212 switch (*s)
214 case 'd':
215 type = S_IFDIR;
216 break;
217 case 'b':
218 type = S_IFBLK;
219 break;
220 case 'c':
221 type = S_IFCHR;
222 break;
223 case 'l':
224 type = S_IFLNK;
225 break;
226 #ifdef S_IFSOCK
227 case 's':
228 type = S_IFSOCK;
229 break;
230 #else
231 case 's':
232 type = S_IFIFO;
233 break;
234 #endif
235 #ifdef S_IFDOOR /* Solaris door */
236 case 'D':
237 type = S_IFDOOR;
238 break;
239 #else
240 case 'D':
241 type = S_IFIFO;
242 break;
243 #endif
244 case 'p':
245 type = S_IFIFO;
246 break;
247 #ifdef S_IFNAM /* Special named files */
248 case 'n':
249 type = S_IFNAM;
250 break;
251 #else
252 case 'n':
253 type = S_IFREG;
254 break;
255 #endif
256 case 'm': /* Don't know what these are :-) */
257 case '-':
258 case '?':
259 type = S_IFREG;
260 break;
261 default:
262 return FALSE;
265 *ret_type = type;
267 if (ret_skipped != NULL)
268 *ret_skipped = 1;
269 return TRUE;
272 /* --------------------------------------------------------------------------------------------- */
274 gboolean
275 vfs_parse_fileperms (const char *s, size_t * ret_skipped, mode_t * ret_perms)
277 const char *p = s;
278 mode_t perms = 0;
280 switch (*p++)
282 case '-':
283 break;
284 case 'r':
285 perms |= S_IRUSR;
286 break;
287 default:
288 return FALSE;
291 switch (*p++)
293 case '-':
294 break;
295 case 'w':
296 perms |= S_IWUSR;
297 break;
298 default:
299 return FALSE;
302 switch (*p++)
304 case '-':
305 break;
306 case 'S':
307 perms |= S_ISUID;
308 break;
309 case 's':
310 perms |= S_IXUSR | S_ISUID;
311 break;
312 case 'x':
313 perms |= S_IXUSR;
314 break;
315 default:
316 return FALSE;
319 switch (*p++)
321 case '-':
322 break;
323 case 'r':
324 perms |= S_IRGRP;
325 break;
326 default:
327 return FALSE;
330 switch (*p++)
332 case '-':
333 break;
334 case 'w':
335 perms |= S_IWGRP;
336 break;
337 default:
338 return FALSE;
341 switch (*p++)
343 case '-':
344 break;
345 case 'S':
346 perms |= S_ISGID;
347 break;
348 case 'l':
349 perms |= S_ISGID;
350 break; /* found on Solaris */
351 case 's':
352 perms |= S_IXGRP | S_ISGID;
353 break;
354 case 'x':
355 perms |= S_IXGRP;
356 break;
357 default:
358 return FALSE;
361 switch (*p++)
363 case '-':
364 break;
365 case 'r':
366 perms |= S_IROTH;
367 break;
368 default:
369 return FALSE;
372 switch (*p++)
374 case '-':
375 break;
376 case 'w':
377 perms |= S_IWOTH;
378 break;
379 default:
380 return FALSE;
383 switch (*p++)
385 case '-':
386 break;
387 case 'T':
388 perms |= S_ISVTX;
389 break;
390 case 't':
391 perms |= S_IXOTH | S_ISVTX;
392 break;
393 case 'x':
394 perms |= S_IXOTH;
395 break;
396 default:
397 return FALSE;
400 if (*p == '+')
401 /* ACLs on Solaris, HP-UX and others */
402 p++;
404 if (ret_skipped != NULL)
405 *ret_skipped = p - s;
406 *ret_perms = perms;
408 return TRUE;
411 /* --------------------------------------------------------------------------------------------- */
413 gboolean
414 vfs_parse_filemode (const char *s, size_t * ret_skipped, mode_t * ret_mode)
416 const char *p = s;
417 mode_t type, perms;
418 size_t skipped;
420 if (!vfs_parse_filetype (p, &skipped, &type))
421 return FALSE;
423 p += skipped;
424 if (!vfs_parse_fileperms (p, &skipped, &perms))
425 return FALSE;
427 p += skipped;
428 *ret_skipped = p - s;
429 *ret_mode = type | perms;
431 return TRUE;
434 /* --------------------------------------------------------------------------------------------- */
436 gboolean
437 vfs_parse_raw_filemode (const char *s, size_t * ret_skipped, mode_t * ret_mode)
439 const char *p = s;
440 mode_t remote_type = 0, local_type, perms = 0;
442 /* isoctal */
443 for (; *p >= '0' && *p <= '7'; p++)
445 perms *= 010;
446 perms += (*p - '0');
449 if (*p++ != ' ')
450 return FALSE;
452 for (; *p >= '0' && *p <= '7'; p++)
454 remote_type *= 010;
455 remote_type += (*p - '0');
458 if (*p++ != ' ')
459 return FALSE;
461 /* generated with:
462 $ perl -e 'use Fcntl ":mode";
463 my @modes = (S_IFDIR, S_IFBLK, S_IFCHR, S_IFLNK, S_IFREG);
464 foreach $t (@modes) { printf ("%o\n", $t); };'
465 TODO: S_IFDOOR, S_IFIFO, S_IFSOCK (if supported by os)
466 (see vfs_parse_filetype)
469 switch (remote_type)
471 case 020000:
472 local_type = S_IFCHR;
473 break;
474 case 040000:
475 local_type = S_IFDIR;
476 break;
477 case 060000:
478 local_type = S_IFBLK;
479 break;
480 case 0120000:
481 local_type = S_IFLNK;
482 break;
483 case 0100000:
484 default: /* don't know what is it */
485 local_type = S_IFREG;
486 break;
489 *ret_skipped = p - s;
490 *ret_mode = local_type | perms;
492 return TRUE;
495 /* --------------------------------------------------------------------------------------------- */
497 gboolean
498 vfs_parse_month (const char *str, struct tm * tim)
500 static const char *month = "JanFebMarAprMayJunJulAugSepOctNovDec";
501 const char *pos;
503 if (str == NULL)
504 return FALSE;
506 pos = strstr (month, str);
507 if (pos == NULL)
508 return FALSE;
510 if (tim != NULL)
511 tim->tm_mon = (pos - month) / 3;
513 return TRUE;
516 /* --------------------------------------------------------------------------------------------- */
517 /** This function parses from idx in the columns[] array */
520 vfs_parse_filedate (int idx, time_t * t)
522 char *p;
523 struct tm tim;
524 int d[3];
525 gboolean got_year = FALSE;
526 gboolean l10n = FALSE; /* Locale's abbreviated month name */
527 time_t current_time;
528 struct tm *local_time;
530 /* Let's setup default time values */
531 current_time = time (NULL);
532 local_time = localtime (&current_time);
533 tim.tm_mday = local_time->tm_mday;
534 tim.tm_mon = local_time->tm_mon;
535 tim.tm_year = local_time->tm_year;
537 tim.tm_hour = 0;
538 tim.tm_min = 0;
539 tim.tm_sec = 0;
540 tim.tm_isdst = -1; /* Let mktime() try to guess correct dst offset */
542 p = columns[idx++];
544 /* We eat weekday name in case of extfs */
545 if (is_week (p, &tim))
546 p = columns[idx++];
549 ALLOWED DATE FORMATS
551 We expect 3 fields max or we'll see oddities with certain file names.
553 Formats that contain either year or time (the default 'ls' formats):
555 * Mon DD hh:mm[:ss]
556 * Mon DD YYYY
558 Formats that contain both year and time, to make it easier to write
559 extfs scripts:
561 * MM-DD-YYYY hh:mm[:ss]
562 * MM-DD-YY hh:mm[:ss]
564 ('/' and '\' can be used instead of '-'.)
566 where Mon is Jan-Dec, DD, MM, YY two digit day, month, year,
567 YYYY four digit year, hh, mm, ss two digit hour, minute or second.
569 (As for the "3 fields max" restriction: this prevents, for example, a
570 file name "13:48" from being considered part of a "Sep 19 2016" date
571 string preceding it.)
574 /* Month name */
575 if (vfs_parse_month (p, &tim))
577 /* And we expect, it followed by day number */
578 if (!is_num (idx))
579 return 0; /* No day */
581 tim.tm_mday = (int) atol (columns[idx++]);
584 else if (is_dos_date (p))
586 /* Case with MM-DD-YY or MM-DD-YYYY */
587 p[2] = p[5] = '-';
589 /* cppcheck-suppress invalidscanf */
590 if (sscanf (p, "%2d-%2d-%d", &d[0], &d[1], &d[2]) != 3)
591 return 0; /* sscanf failed */
593 /* Months are zero based */
594 if (d[0] > 0)
595 d[0]--;
597 if (d[2] > 1900)
598 d[2] -= 1900;
599 else if (d[2] < 70)
600 /* Y2K madness */
601 d[2] += 100;
603 tim.tm_mon = d[0];
604 tim.tm_mday = d[1];
605 tim.tm_year = d[2];
606 got_year = TRUE;
608 else if (is_localized_month (p) && is_num (idx++))
609 /* Locale's abbreviated month name followed by day number */
610 l10n = TRUE;
611 else
612 return 0; /* unsupported format */
614 /* Here we expect to find time or year */
615 if (!is_num (idx)
616 || !(is_time (columns[idx], &tim) || (got_year = is_year (columns[idx], &tim))))
617 return 0; /* Neither time nor date */
619 idx++;
622 * If the date is less than 6 months in the past, it is shown without year
623 * other dates in the past or future are shown with year but without time
624 * This does not check for years before 1900 ... I don't know, how
625 * to represent them at all
627 if (!got_year && local_time->tm_mon < 6 && local_time->tm_mon < tim.tm_mon
628 && tim.tm_mon - local_time->tm_mon >= 6)
629 tim.tm_year--;
631 *t = mktime (&tim);
632 if (l10n || (*t < 0))
633 *t = 0;
635 return idx;
638 /* --------------------------------------------------------------------------------------------- */
641 vfs_split_text (char *p)
643 char *original = p;
644 int numcols;
646 memset (columns, 0, sizeof (columns));
648 for (numcols = 0; *p != '\0' && numcols < MAXCOLS; numcols++)
650 for (; *p == ' ' || *p == '\r' || *p == '\n'; p++)
651 *p = '\0';
653 columns[numcols] = p;
654 column_ptr[numcols] = p - original;
656 for (; *p != '\0' && *p != ' ' && *p != '\r' && *p != '\n'; p++)
660 return numcols;
663 /* --------------------------------------------------------------------------------------------- */
665 void
666 vfs_parse_ls_lga_init (void)
668 vfs_parce_ls_final_num_spaces = 1;
671 /* --------------------------------------------------------------------------------------------- */
673 size_t
674 vfs_parse_ls_lga_get_final_spaces (void)
676 return vfs_parce_ls_final_num_spaces;
679 /* --------------------------------------------------------------------------------------------- */
681 gboolean
682 vfs_parse_ls_lga (const char *p, struct stat * s, char **filename, char **linkname,
683 size_t * num_spaces)
685 int idx, idx2, num_cols;
686 int i;
687 char *p_copy = NULL;
688 char *t = NULL;
689 const char *line = p;
690 size_t skipped;
692 if (strncmp (p, "total", 5) == 0)
693 return FALSE;
695 if (!vfs_parse_filetype (p, &skipped, &s->st_mode))
696 goto error;
698 p += skipped;
699 if (*p == ' ') /* Notwell 4 */
700 p++;
701 if (*p == '[')
703 if (strlen (p) <= 8 || p[8] != ']')
704 goto error;
706 /* Should parse here the Notwell permissions :) */
707 if (S_ISDIR (s->st_mode))
708 s->st_mode |= (S_IRUSR | S_IRGRP | S_IROTH | S_IWUSR | S_IXUSR | S_IXGRP | S_IXOTH);
709 else
710 s->st_mode |= (S_IRUSR | S_IRGRP | S_IROTH | S_IWUSR);
711 p += 9;
713 else
715 size_t lc_skipped;
716 mode_t perms;
718 if (!vfs_parse_fileperms (p, &lc_skipped, &perms))
719 goto error;
721 p += lc_skipped;
722 s->st_mode |= perms;
725 p_copy = g_strdup (p);
726 num_cols = vfs_split_text (p_copy);
728 s->st_nlink = atol (columns[0]);
729 if (s->st_nlink <= 0)
730 goto error;
732 if (!is_num (1))
733 s->st_uid = vfs_finduid (columns[1]);
734 else
735 s->st_uid = (uid_t) atol (columns[1]);
737 /* Mhm, the ls -lg did not produce a group field */
738 for (idx = 3; idx <= 5; idx++)
739 if (vfs_parse_month (columns[idx], NULL) || is_week (columns[idx], NULL)
740 || is_dos_date (columns[idx]) || is_localized_month (columns[idx]))
741 break;
743 if (idx == 6 || (idx == 5 && !S_ISCHR (s->st_mode) && !S_ISBLK (s->st_mode)))
744 goto error;
746 /* We don't have gid */
747 if (idx == 3 || (idx == 4 && (S_ISCHR (s->st_mode) || S_ISBLK (s->st_mode))))
748 idx2 = 2;
749 else
751 /* We have gid field */
752 if (is_num (2))
753 s->st_gid = (gid_t) atol (columns[2]);
754 else
755 s->st_gid = vfs_findgid (columns[2]);
756 idx2 = 3;
759 /* This is device */
760 if (S_ISCHR (s->st_mode) || S_ISBLK (s->st_mode))
762 int maj, min;
764 /* Corner case: there is no whitespace(s) between maj & min */
765 if (!is_num (idx2) && idx2 == 2)
767 /* cppcheck-suppress invalidscanf */
768 if (!is_num (++idx2) || sscanf (columns[idx2], " %d,%d", &maj, &min) != 2)
769 goto error;
771 else
773 /* cppcheck-suppress invalidscanf */
774 if (!is_num (idx2) || sscanf (columns[idx2], " %d,", &maj) != 1)
775 goto error;
777 /* cppcheck-suppress invalidscanf */
778 if (!is_num (++idx2) || sscanf (columns[idx2], " %d", &min) != 1)
779 goto error;
781 #ifdef HAVE_STRUCT_STAT_ST_RDEV
782 s->st_rdev = makedev (maj, min);
783 #endif
784 s->st_size = 0;
787 else
789 /* Common file size */
790 if (!is_num (idx2))
791 goto error;
793 s->st_size = (off_t) g_ascii_strtoll (columns[idx2], NULL, 10);
794 #ifdef HAVE_STRUCT_STAT_ST_RDEV
795 s->st_rdev = 0;
796 #endif
799 idx = vfs_parse_filedate (idx, &s->st_mtime);
800 if (idx == 0)
801 goto error;
803 /* Use resulting time value */
804 s->st_atime = s->st_ctime = s->st_mtime;
805 #ifdef HAVE_STRUCT_STAT_ST_MTIM
806 s->st_atim.tv_nsec = s->st_mtim.tv_nsec = s->st_ctim.tv_nsec = 0;
807 #endif
809 /* s->st_dev and s->st_ino must be initialized by vfs_s_new_inode () */
810 #ifdef HAVE_STRUCT_STAT_ST_BLKSIZE
811 s->st_blksize = 512;
812 #endif
813 vfs_adjust_stat (s);
815 if (num_spaces != NULL)
817 *num_spaces = column_ptr[idx] - column_ptr[idx - 1] - strlen (columns[idx - 1]);
818 if (DIR_IS_DOTDOT (columns[idx]))
819 vfs_parce_ls_final_num_spaces = *num_spaces;
822 for (i = idx + 1, idx2 = 0; i < num_cols; i++)
823 if (strcmp (columns[i], "->") == 0)
825 idx2 = i;
826 break;
829 if (((S_ISLNK (s->st_mode) || (num_cols == idx + 3 && s->st_nlink > 1))) /* Maybe a hardlink? (in extfs) */
830 && idx2 != 0)
832 if (filename != NULL)
833 *filename = g_strndup (p + column_ptr[idx], column_ptr[idx2] - column_ptr[idx] - 1);
835 if (linkname != NULL)
837 t = g_strdup (p + column_ptr[idx2 + 1]);
838 *linkname = t;
841 else
843 /* Extract the filename from the string copy, not from the columns
844 * this way we have a chance of entering hidden directories like ". ."
846 if (filename != NULL)
848 /* filename = g_strdup (columns [idx++]); */
849 t = g_strdup (p + column_ptr[idx]);
850 *filename = t;
853 if (linkname != NULL)
854 *linkname = NULL;
857 if (t != NULL)
859 size_t p2;
861 p2 = strlen (t);
862 if (--p2 > 0 && (t[p2] == '\r' || t[p2] == '\n'))
863 t[p2] = '\0';
864 if (--p2 > 0 && (t[p2] == '\r' || t[p2] == '\n'))
865 t[p2] = '\0';
868 g_free (p_copy);
869 return TRUE;
871 error:
873 static int errorcount = 0;
875 if (++errorcount < 5)
876 message (D_ERROR, _("Cannot parse:"), "%s",
877 (p_copy != NULL && *p_copy != '\0') ? p_copy : line);
878 else if (errorcount == 5)
879 message (D_ERROR, MSG_ERROR, _("More parsing errors will be ignored."));
882 g_free (p_copy);
883 return FALSE;
886 /* --------------------------------------------------------------------------------------------- */