1 // =================================
  2 // Copyright (c) 2022 Seppo Laakko
  3 // Distributed under the MIT license
  4 // =================================
  5 
  6 using System;
  7 using System.Collections;
  8 using System.IO;
  9 using System.Text;
 10 using System.Security;
 11 
 12 class Credentials
 13 {
 14     static Credentials() : instance(new Credentials())
 15     {
 16     }
 17     public static Credentials& Instance()
 18     {
 19         return *instance;
 20     }
 21     private Credentials()
 22     {
 23         users = GetUsers();
 24         groups = GetGroups();
 25     }
 26     public string GetUserName(int uid) const
 27     {
 28         User* user = users.GetUser(uid);
 29         if (user != null)
 30         {
 31             return user->Name();
 32         }
 33         return string();
 34     }
 35     public string GetGroupName(int gid) const
 36     {
 37         Group* group = groups.GetGroup(gid);
 38         if (group != null)
 39         {
 40             return group->Name();
 41         }
 42         return string();
 43     }
 44     private static UniquePtr<Credentials> instance;
 45     private Users users;
 46     private Groups groups;
 47 }
 48 
 49 enum Fields
 50 {
 51     none = 0fs = 1 << 0inode = 1 << 1access = 1 << 2nlinks = 1 << 3user = 1 << 4uid = 1 << 5group = 1 << 6gid = 1 << 7size = 1 << 8
 52     ctime = 1 << 9mtime = 1 << 10atime = 1 << 11name = 1 << 12
 53 }
 54 
 55 class FieldValue
 56 {
 57     public nothrow FieldValue(const ustring& text_Fields field_) : text(text_)length(cast<int>(text.Length()))field(field_)
 58     {
 59     }
 60     public ustring text;
 61     public int length;
 62     public Fields field;
 63 }
 64 
 65 enum FileKind
 66 {
 67     regularexecutabledirectoryfifo
 68 }
 69 
 70 enum Flags
 71 {
 72     none = 0all = 1 << 0longFormat = 1 << 1humanReadableSize = 1 << 2hasColors = 1 << 3reverse = 1 << 4
 73 }
 74 
 75 FieldValue MakeIntFieldValue(int nFields field)
 76 {
 77     return FieldValue(ToUtf32(ToString(n))field);
 78 }
 79 
 80 FieldValue MakeStringFieldValue(const string& sFields field)
 81 {
 82     return FieldValue(ToUtf32(s)field);
 83 }
 84 
 85 FieldValue MakeUStringFieldValue(const ustring& sFields field)
 86 {
 87     return FieldValue(sfield);
 88 }
 89 
 90 FieldValue MakeTimeFieldValue(const DateTime& dateTimeFields field)
 91 {
 92     return FieldValue(ToUtf32(dateTime.ToString())field);
 93 }
 94 
 95 FieldValue MakeSizeFieldValue(long sizebool humanReadableSize)
 96 {
 97     if (!humanReadableSize)
 98     {
 99         return FieldValue(ToUtf32(ToString(size))Fields.size);
100     }
101     else
102     {
103         long g = size / 1024 / 1024 / 1024;
104         if (g > 0)
105         {
106             long s = (size - 1) / 1024 / 1024 / 1024 + 1;
107             return FieldValue(ToUtf32(ToString(s) + "G")Fields.size);
108         }
109         else
110         {
111             long m = size / 1024 / 1024;
112             if (m > 0)
113             {
114                 long s = (size - 1) / 1024 / 1024 + 1;
115                 return FieldValue(ToUtf32(ToString(s) + "M")Fields.size);
116             }
117             else
118             {
119                 long k = size / 1024;
120                 if (k > 0)
121                 {
122                     long s = (size - 1) / 1024 + 1;
123                     return FieldValue(ToUtf32(ToString(s) + "K")Fields.size);
124                 }
125                 else
126                 {
127                     return FieldValue(ToUtf32(ToString(size))Fields.size);
128                 }
129             }
130         }
131     }
132 }
133 
134 string AccessStr(Access accessbool setUIDorSetGIDBit)
135 {
136     string accessStr;
137     if ((access & Access.read) != Access.none)
138     {
139         accessStr.Append('r');
140     }
141     else
142     {
143         accessStr.Append('-');
144     }
145     if ((access & Access.write) != Access.none)
146     {
147         accessStr.Append('w');
148     }
149     else
150     {
151         accessStr.Append('-');
152     }
153     if (setUIDorSetGIDBit)
154     {
155         accessStr.Append('s');
156     }
157     else
158     {
159         if ((access & Access.execute) != Access.none)
160         {
161             accessStr.Append('x');
162         }
163         else
164         {
165             accessStr.Append('-');
166         }
167     }
168     return accessStr;
169 }
170 
171 FieldValue MakeAccessFieldValue(const FileStatus& fileStatus)
172 {
173     string accessStr;
174     if (fileStatus.fileType == FileType.directory)
175     {
176         accessStr.Append('d');
177     }
178     else
179     {
180         accessStr.Append('-');
181     }
182     accessStr.Append(AccessStr(fileStatus.ownerAccessfileStatus.setUID));
183     accessStr.Append(AccessStr(fileStatus.groupAccessfileStatus.setGID));
184     accessStr.Append(AccessStr(fileStatus.otherAccessfalse));
185     return FieldValue(ToUtf32(accessStr)Fields.access);
186 }
187 
188 FormatJustify GetFieldJustification(Fields field)
189 {
190     switch (field)
191     {
192         case Fields.fs:
193         case Fields.inode:
194         case Fields.nlinks:
195         case Fields.uid:
196         case Fields.gid:
197         case Fields.size:
198         {
199             return FormatJustify.right;
200         }
201     }
202     return FormatJustify.left;
203 }
204 
205 nothrow Fields ShortFields()
206 {
207     return Fields.name;
208 }
209 
210 nothrow Fields LongFields()
211 {
212     return cast<Fields>(Fields.access | Fields.nlinks | Fields.user | Fields.group | Fields.size | Fields.mtime | Fields.name);
213 }
214 
215 class FileInfo
216 {
217     public nothrow FileInfo(const FileStatus& fileStatus_const DirectoryEntry& entry_) : fileStatus(fileStatus_)entry(entry_)
218     {
219     }
220     public int FieldWidth(int fieldNr) const
221     {
222         if (fieldNr >= 0 && fieldNr < fieldValues.Count())
223         {
224             return fieldValues[fieldNr].length;
225         }
226         return 0;
227     }
228     public FileKind Kind() const
229     {
230         if (fileStatus.fileType == FileType.directory) return FileKind.directory;
231         if (fileStatus.fileType == FileType.fifo) return FileKind.fifo;
232         if (fileStatus.fileType == FileType.regular)
233         {
234             if ((fileStatus.ownerAccess & Access.execute) != Access.none)
235             {
236                 return FileKind.executable;
237             }
238         }
239         return FileKind.regular;
240     }
241     public const FieldValue& GetFieldValue(int fieldIndex) const
242     {
243         return fieldValues[fieldIndex];
244     }
245     public FileStatus fileStatus;
246     public DirectoryEntry entry;
247     public List<FieldValue> fieldValues;
248     public long nameFieldIndex;
249     public ustring userName;
250     public ustring groupName;
251 }
252 
253 enum SortField
254 {
255     nameinodenlinksusersizetime
256 }
257 
258 class SortBy : Rel<FileInfo>
259 {
260     public SortBy(SortField sortField_) : sortField(sortField_)
261     {
262     }
263     public bool operator()(const FileInfo& leftconst FileInfo& right) const
264     {
265         switch (sortField)
266         {
267             case SortField.inode:
268             {
269                 if (left.fileStatus.inodeNumber < right.fileStatus.inodeNumber) return true;
270                 if (left.fileStatus.inodeNumber > right.fileStatus.inodeNumber) return false;
271                 break;
272             }
273             case SortField.nlinks:
274             {
275                 if (left.fileStatus.nlinks < right.fileStatus.nlinks) return true;
276                 if (left.fileStatus.nlinks > right.fileStatus.nlinks) return false;
277                 break;
278             }
279             case SortField.user:
280             {
281                 if (left.userName < right.userName) return true;
282                 if (left.userName > right.userName) return false;
283                 break;
284             }
285             case SortField.size:
286             {
287                 if (left.fileStatus.fileSize > right.fileStatus.fileSize) return true;
288                 if (left.fileStatus.fileSize < right.fileStatus.fileSize) return false;
289                 break;
290             }
291             case SortField.time:
292             {
293                 if (left.fileStatus.mtime < right.fileStatus.mtime) return true;
294                 if (left.fileStatus.mtime > right.fileStatus.mtime) return false;
295                 break;
296             }
297         }
298         return left.fieldValues[left.nameFieldIndex].text < right.fieldValues[right.nameFieldIndex].text;
299     }
300     private SortField sortField;
301 }
302 
303 void Sort(List<FileInfo>& fileInfosSortField sortField)
304 {
305     System.Sort(fileInfosSortBy(sortField));
306 }
307 
308 string GetColorString(const FileInfo& fileInfobool hasColors)
309 {
310     if (!hasColors)
311     {
312         return string();
313     }
314     switch (fileInfo.Kind())
315     {
316         case FileKind.regular:
317         {
318             return SetColors(ConsoleColor.whiteDefaultConsoleBackColor());
319         }
320         case FileKind.executable:
321         {
322             return SetColors(ConsoleColor.greenDefaultConsoleBackColor());
323         }
324         case FileKind.directory:
325         {
326             return SetColors(ConsoleColor.cyanDefaultConsoleBackColor());
327         }
328         case FileKind.fifo:
329         {
330             return SetColors(ConsoleColor.redDefaultConsoleBackColor());
331         }
332     }
333     return SetColors(ConsoleColor.whiteDefaultConsoleBackColor());
334 }
335 
336 string ResetColorString(bool hasColors)
337 {
338     if (!hasColors)
339     {
340         return string();
341     }
342     return ResetColors();
343 }
344 
345 void MakeFileInfo(FileInfo& fileInfoFields fieldsbool humanReadableSize)
346 {
347     if ((fields & Fields.fs) != Fields.none)
348     {
349         fileInfo.fieldValues.Add(MakeIntFieldValue(fileInfo.fileStatus.fileSystemNumberFields.fs));
350     }
351     if ((fields & Fields.inode) != Fields.none)
352     {
353         fileInfo.fieldValues.Add(MakeIntFieldValue(fileInfo.fileStatus.inodeNumberFields.inode));
354     }
355     if ((fields & Fields.access) != Fields.none)
356     {
357         fileInfo.fieldValues.Add(MakeAccessFieldValue(fileInfo.fileStatus));
358     }
359     if ((fields & Fields.nlinks) != Fields.none)
360     {
361         fileInfo.fieldValues.Add(MakeIntFieldValue(fileInfo.fileStatus.nlinksFields.nlinks));
362     }
363     if ((fields & Fields.user) != Fields.none)
364     {
365         fileInfo.userName = ToUtf32(Credentials.Instance().GetUserName(fileInfo.fileStatus.uid));
366         fileInfo.fieldValues.Add(MakeUStringFieldValue(fileInfo.userNameFields.user));
367     }
368     if ((fields & Fields.uid) != Fields.none)
369     {
370         fileInfo.fieldValues.Add(MakeIntFieldValue(fileInfo.fileStatus.uidFields.uid));
371     }
372     if ((fields & Fields.group) != Fields.none)
373     {
374         fileInfo.groupName = ToUtf32(Credentials.Instance().GetGroupName(fileInfo.fileStatus.gid));
375         fileInfo.fieldValues.Add(MakeUStringFieldValue(fileInfo.groupNameFields.group));
376     }
377     if ((fields & Fields.gid) != Fields.none)
378     {
379         fileInfo.fieldValues.Add(MakeIntFieldValue(fileInfo.fileStatus.gidFields.gid));
380     }
381     if ((fields & Fields.size) != Fields.none)
382     {
383         fileInfo.fieldValues.Add(MakeSizeFieldValue(fileInfo.fileStatus.fileSizehumanReadableSize));
384     }
385     if ((fields & Fields.ctime) != Fields.none)
386     {
387         fileInfo.fieldValues.Add(MakeTimeFieldValue(fileInfo.fileStatus.ctimeFields.ctime));
388     }
389     if ((fields & Fields.mtime) != Fields.none)
390     {
391         fileInfo.fieldValues.Add(MakeTimeFieldValue(fileInfo.fileStatus.mtimeFields.mtime));
392     }
393     if ((fields & Fields.atime) != Fields.none)
394     {
395         fileInfo.fieldValues.Add(MakeTimeFieldValue(fileInfo.fileStatus.atimeFields.atime));
396     }
397     if ((fields & Fields.name) != Fields.none)
398     {
399         fileInfo.nameFieldIndex = fileInfo.fieldValues.Count();
400         fileInfo.fieldValues.Add(MakeStringFieldValue(fileInfo.entry.nameFields.name));
401     }
402 }
403 
404 void MakeFileInfos(List<FileInfo>& fileInfosFields fieldsbool humanReadableSize)
405 {
406     for (FileInfo& fileInfo : fileInfos)
407     {
408         MakeFileInfo(fileInfofieldshumanReadableSize);
409     }
410 }
411 
412 int NumberOfLines(int numberOfFileInfosint numberOfColumns)
413 {
414     if (numberOfFileInfos > 0)
415     {
416         return (numberOfFileInfos - 1) / numberOfColumns + 1;
417     }
418     else
419     {
420         return 0;
421     }
422 }
423 
424 int NumberOfFields(Fields fields)
425 {
426     int nfields = 0;
427     if ((fields & Fields.fs) != Fields.none)
428     {
429         ++nfields;
430     }
431     if ((fields & Fields.inode) != Fields.none)
432     {
433         ++nfields;
434     }
435     if ((fields & Fields.access) != Fields.none)
436     {
437         ++nfields;
438     }
439     if ((fields & Fields.nlinks) != Fields.none)
440     {
441         ++nfields;
442     }
443     if ((fields & Fields.user) != Fields.none)
444     {
445         ++nfields;
446     }
447     if ((fields & Fields.uid) != Fields.none)
448     {
449         ++nfields;
450     }
451     if ((fields & Fields.group) != Fields.none)
452     {
453         ++nfields;
454     }
455     if ((fields & Fields.gid) != Fields.none)
456     {
457         ++nfields;
458     }
459     if ((fields & Fields.size) != Fields.none)
460     {
461         ++nfields;
462     }
463     if ((fields & Fields.ctime) != Fields.none)
464     {
465         ++nfields;
466     }
467     if ((fields & Fields.mtime) != Fields.none)
468     {
469         ++nfields;
470     }
471     if ((fields & Fields.atime) != Fields.none)
472     {
473         ++nfields;
474     }
475     if ((fields & Fields.name) != Fields.none)
476     {
477         ++nfields;
478     }
479     return nfields;
480 }
481 
482 int CalculateFieldWidth(const List<FileInfo>& fileInfosint columnNrint fieldNrint numberOfLines)
483 {
484     int fieldWidth = 0;
485     for (int i = 0; i < numberOfLines; ++i;)
486     {
487         int fileInfoIndex = columnNr * numberOfLines + i;
488         if (fileInfoIndex < fileInfos.Count())
489         {
490             const FileInfo& fileInfo = fileInfos[fileInfoIndex];
491             int w = fileInfo.FieldWidth(fieldNr);
492             if (w > fieldWidth)
493             {
494                 fieldWidth = w;
495             }
496         }
497     }
498     return fieldWidth;
499 }
500 
501 int CalculateColumnWidth(const List<FileInfo>& fileInfosint columnNrint numberOfLinesint numberOfFields)
502 {
503     int columnWidth = 0;
504     for (int f = 0; f < numberOfFields; ++f;)
505     {
506         if (f > 0)
507         {
508             ++columnWidth;
509         }
510         int fieldWidth = CalculateFieldWidth(fileInfoscolumnNrfnumberOfLines);
511         columnWidth = columnWidth + fieldWidth;
512     }
513     return columnWidth;
514 }
515 
516 int CalculateLineLength(const List<FileInfo>& fileInfosint numberOfColumnsint numberOfFieldsint screenWidth)
517 {
518     int lineLength = 0;
519     int numberOfLines = NumberOfLines(cast<int>(fileInfos.Count())numberOfColumns);
520     for (int c = 0; c < numberOfColumns; ++c;)
521     {
522         if (c > 0)
523         {
524             ++lineLength;
525             ++lineLength;
526         }
527         int columnWidth = CalculateColumnWidth(fileInfoscnumberOfLinesnumberOfFields);
528         lineLength = lineLength + columnWidth;
529         if (lineLength >= screenWidth)
530         {
531             break;
532         }
533     }
534     return lineLength;
535 }
536 
537 string MakeLine(const List<FileInfo>& fileInfosint lineNrint numberOfLinesint numberOfColumnsint numberOfFieldsbool hasColorsconst Map<Pair<intint>int>& fieldWidths)
538 {
539     string line;
540     for (int c = 0; c < numberOfColumns; ++c;)
541     {
542         if (c > 0)
543         {
544             line.Append("  ");
545         }
546         int fileInfoIndex = c * numberOfLines + lineNr;
547         if (fileInfoIndex < fileInfos.Count())
548         {
549             const FileInfo& fileInfo = fileInfos[fileInfoIndex];
550             for (int f = 0; f < numberOfFields; ++f;)
551             {
552                 if (f > 0)
553                 {
554                     line.Append(' ');
555                 }
556                 const FieldValue& fieldValue = fileInfo.GetFieldValue(f);
557                 FormatJustify justify = GetFieldJustification(fieldValue.field);
558                 ustring text = Format(fieldValue.textfieldWidths[MakePair(cf)]justify);
559                 if (fieldValue.field == Fields.name)
560                 {
561                     line.Append(GetColorString(fileInfohasColors)).Append(ToUtf8(text)).Append(ResetColorString(hasColors));
562                 }
563                 else
564                 {
565                     line.Append(ToUtf8(text));
566                 }
567             }
568         }
569     }
570     return line;
571 }
572 
573 List<string> MakeLines(List<FileInfo>& fileInfosSortField sortFieldFields fieldsFlags flagsint screenWidth)
574 {
575     MakeFileInfos(fileInfosfields(flags & Flags.humanReadableSize) != Flags.none);
576     Sort(fileInfossortField);
577     if ((flags & Flags.reverse) != Flags.none)
578     {
579         Reverse(fileInfos.Begin()fileInfos.End());
580     }
581     int numberOfFields = NumberOfFields(fields);
582     int numberOfColumns = 1;
583     int lineLength = 0;
584     int ncols = numberOfColumns;
585     if (screenWidth > 0 && (flags & Flags.longFormat) == Flags.none)
586     {
587         while (ncols < screenWidth)
588         {
589             int len = CalculateLineLength(fileInfosncolsnumberOfFieldsscreenWidth);
590             if (len >= screenWidth)
591             {
592                 break;
593             }
594             else
595             {
596                 numberOfColumns = ncols;
597                 lineLength = len;
598                 ++ncols;
599             }
600         }
601     }
602     int numberOfLines = NumberOfLines(cast<int>(fileInfos.Count())numberOfColumns);
603     Map<Pair<intint>int> fieldWidths;
604     for (int c = 0; c < numberOfColumns; ++c;)
605     {
606         for (int f = 0; f < numberOfFields; ++f;)
607         {
608             int w = CalculateFieldWidth(fileInfoscfnumberOfLines);
609             fieldWidths[MakePair(cf)] = w;
610         }
611     }
612     List<string> lines;
613     for (int i = 0; i < numberOfLines; ++i;)
614     {
615         lines.Add(MakeLine(fileInfosinumberOfLinesnumberOfColumnsnumberOfFields(flags & Flags.hasColors) != Flags.nonefieldWidths));
616     }
617     return lines;
618 }
619 
620 void PrintLines(const List<string>& lines)
621 {
622     for (const string& line : lines)
623     {
624         Console.Out() << line << endl();
625     }
626 }
627 
628 void ListFiles(const List<string>& filesSortField sortFieldFields additionalFieldsFlags flagsint screenWidth)
629 {
630     Fields fields;
631     if ((flags & Flags.longFormat) != Flags.none)
632     {
633         fields = LongFields();
634     }
635     else
636     {
637         fields = ShortFields();
638     }
639     fields = cast<Fields>(fields | additionalFields);
640     List<FileInfo> fileInfos;
641     DirectoryEntry dirEntry;
642     for (const string& file : files)
643     {
644         FileStatus fileStatus;
645         Stat(file.Chars()fileStatus);
646         DirectoryEntry entry(fileStatus.inodeNumberfile);
647         if (fileStatus.fileType == FileType.regular)
648         {
649             fileInfos.Add(FileInfo(fileStatusentry));
650         }
651         else if (fileStatus.fileType == FileType.directory)
652         {
653             List<FileInfo> dirFileInfos;
654             DirectoryReader reader(file);
655             while (reader.Read(dirEntry))
656             {
657                 if (dirEntry.name.StartsWith("."))
658                 {
659                     if ((flags & Flags.all) == Flags.none)
660                     {
661                         continue;
662                     }
663                 }
664                 string path = Path.Combine(filedirEntry.name);
665                 FileStatus status;
666                 Stat(path.Chars()status);
667                 dirFileInfos.Add(FileInfo(statusdirEntry));
668             }
669             if (!dirFileInfos.IsEmpty())
670             {
671                 List<string> lines = MakeLines(dirFileInfossortFieldfieldsflagsscreenWidth);
672                 PrintLines(lines);
673             }
674         }
675     }
676     if (!fileInfos.IsEmpty())
677     {
678         List<string> lines = MakeLines(fileInfossortFieldfieldsflagsscreenWidth);
679         PrintLines(lines);
680     }
681 }