1 /* This file is part of the Amalthea library.
2  *
3  * Copyright (C) 2018-2024 Eugene 'Vindex' Stulin
4  *
5  * Distributed under the Boost Software License 1.0 or (at your option)
6  * the GNU Lesser General Public License 3.0 or later.
7  */
8 
9 module amalthea.fs;
10 
11 import core.sys.posix.sys.stat,
12        core.sys.posix.fcntl,
13        core.sys.posix.dirent,
14        core.stdc.errno;
15 import core.sys.linux.sys.xattr : getxattr, setxattr, listxattr, removexattr;
16 import unix_unistd = core.sys.posix.unistd;
17 import unix_stat = core.sys.posix.sys.stat;
18 
19 import std.conv : oct = octal;
20 import std.stdio : File;
21 import std.file, std.path;
22 public import std.typecons;
23 
24 import std.algorithm, std.array, std.datetime.systime, std.string;
25 import std.regex : regex, matchFirst;
26 import std.algorithm.iteration : splitter;
27 
28 public import amalthea.libcore;
29 import amalthea.sys, amalthea.time;
30 import amalthea.dataprocessing : isAmong;
31 
32 alias mv  = std.file.rename;
33 alias cd  = std.file.chdir;
34 alias pwd = getcwd;
35 alias rm  = std.file.remove;
36 
37 
38 immutable size_t eloopLimit = 128;
39 
40 
41 /*******************************************************************************
42  * Checks if the path is an empty directory.
43  */
44 bool isEmptyDir(string dirPath) {
45     if (!isDir(dirPath)) {
46         return false;
47     }
48     foreach(FileEntry ent; DirReader(dirPath)) {
49         string name = baseName(ent.path);
50         if (name != "." && name != "..") {
51             return false;
52         }
53     }
54     return true;
55 }
56 
57 
58 /*******************************************************************************
59  * Implementation of file list reader.
60  * Can be used in foreach as input range, the generator returns FileEntry.
61  */
62 struct DirReader {
63     this(string dir) {
64         this.directory = dir;
65         errno = 0;
66         this.dirstream = opendir(dir.toStringz);
67         enforce(errno == 0, new FileException(
68             dir, "Failed to open directory: " ~ getInfoAboutError(errno))
69         );
70         this.popFront();
71     }
72 
73     ~this() {
74         closedir(dirstream);
75     }
76 
77     @property empty() const {
78         return this.currentDirent is null;
79     }
80 
81     FileEntry popFront() {
82         assert(!end);
83         auto prevFileEntry = this.currentFileEntry;
84         errno = 0;
85         this.currentDirent = readdir(dirstream);
86         enforce(
87             !errno,
88             new FileException(this.directory, getInfoAboutError(errno))
89         );
90         if (this.currentDirent !is null) {
91             string name = (*currentDirent).d_name.ptr.fromStringz.idup;
92             string path = buildPath(this.directory, name);
93             this.currentFileEntry = FileEntry(path);
94         } else {
95             end = true;
96         }
97         return prevFileEntry;
98     }
99 
100     FileEntry front() const {
101         assert(this.currentDirent !is null);
102         return this.currentFileEntry;
103     }
104 
105 private:
106 
107     dirent* currentDirent;
108     DIR* dirstream;
109     string directory;
110     FileEntry currentFileEntry;
111     bool end = false;
112 }
113 
114 
115 /*******************************************************************************
116  * Reads entries of the directory.
117  * Params:
118  *     dir              = Directory for listing files.
119  *     hidden           = Whether to include hidden files. Yes, by default.
120  *     recursively      = Whether to search in subdirectories. No, by default.
121  *     saveAbsolutePath = Whether to store an absolute path or not. If Yes,
122  *                        a relative path of dir is converted to absolute.
123  *                        The parameter has no effect if dir is an absolute
124  *                        path. Has value Yes, by default.
125  *     enableFilter     = Whether to filter by type. No, by default.
126  *     includedTypes = Types for including to final result.
127  *                     Used in conjunction with the previous parameter.
128  * See_Also:
129  *     $(FS_REF getRegularFiles),
130  *     $(FS_REF getDirectories),
131  *     $(FS_REF getSymlinks).
132  */
133 FileEntry[] getFiles(string dir,
134                      Flag!"hidden" hidden = Yes.hidden,
135                      Flag!"recursively" recursively = No.recursively,
136                      Flag!"absPath" saveAbsolutePath = Yes.absPath,
137                      Flag!"enableFilter" enableFilter = No.enableFilter,
138                      FileType[] includedTypes = []) {
139     dir = saveAbsolutePath ? getAbsolutePath(dir) : dir;
140     errno = 0;
141     DIR* dirstream = opendir(dir.toStringz);
142     string errInfo = "Failed to open directory: " ~ getInfoAboutError(errno);
143     enforce(!errno, new FileException(dir, errInfo));
144     scope(exit) closedir(dirstream);
145     dirent* dirEntry = readdir(dirstream);
146     enforce(!errno, new FileException(dir, getInfoAboutError(errno)));
147     FileEntry[] entries;
148     for (; dirEntry; dirEntry = readdir(dirstream)) {
149         string name = (*dirEntry).d_name.ptr.fromStringz.idup;
150         if (hidden && name.isAmong(".", "..")) continue;
151         if (!hidden && name.startsWith('.')) continue;
152         string path = buildPath(dir, name);
153         FileEntry entry = FileEntry(path);
154         if (recursively && entry.type == FileType.directory) {
155             entries ~= getFiles(
156                 path,
157                 hidden,
158                 recursively,
159                 saveAbsolutePath,
160                 enableFilter,
161                 includedTypes
162             );
163         }
164 
165         if (!enableFilter || entry.type.isAmong(includedTypes)) {
166             entries ~= entry;
167         }
168     }
169     enforce(!errno, new FileException(dir, errno));
170     return entries;
171 }
172 
173 
174 /*******************************************************************************
175  * Gets regular files from the directory.
176  * Params:
177  *     dir              = Directory for listing regular files.
178  *     hidden           = Whether to include hidden files. Yes, by default.
179  *     recursively      = Whether to search in subdirectories. No, by default.
180  *     saveAbsolutePath = Whether to write the full path. Yes, by default.
181  */
182 FileEntry[] getRegularFiles(string dir,
183                             Flag!"hidden" hidden = Yes.hidden,
184                             Flag!"recursively" recursively = No.recursively,
185                             Flag!"absPath" saveAbsolutePath = Yes.absPath) {
186     FileType[] includedTypes = [FileType.regularFile];
187     return getFiles(dir, hidden, recursively, saveAbsolutePath,
188                     Yes.enableFilter, includedTypes);
189 }
190 
191 
192 /*******************************************************************************
193  * Gets directories from the directory.
194  * Params:
195  *     dir         = Directory for listing directories.
196  *     hidden      = Whether to include hidden files. Yes, by default.
197  *     recursively = Whether to search in subdirectories. No, by default.
198  *     saveAbsolutePath = Whether to write the full path. Yes, by default.
199  */
200 FileEntry[] getDirectories(string dir,
201                            Flag!"hidden" hidden = Yes.hidden,
202                            Flag!"recursively" recursively = No.recursively,
203                            Flag!"absPath" saveAbsolutePath = Yes.absPath) {
204     FileType[] includedTypes = [FileType.directory];
205     return getFiles(dir, hidden, recursively, saveAbsolutePath,
206                     Yes.enableFilter, includedTypes);
207 }
208 
209 
210 /*******************************************************************************
211  * Gets symlinks from the directory.
212  * Params:
213  *     dir           = Directory for listing symlinks.
214  *     hidden        = Whether to include hidden files. Yes, by default.
215  *     recursively   = Whether to search in subdirectories. No, by default.
216  *     saveAbsolutePath = Whether to write the full path. Yes, by default.
217  */
218 FileEntry[] getSymlinks(string dir,
219                         Flag!"hidden" hidden = Yes.hidden,
220                         Flag!"recursively" recursively = No.recursively,
221                         Flag!"absPath" saveAbsolutePath = Yes.absPath) {
222     FileType[] includedTypes = [FileType.symlink];
223     return getFiles(dir, hidden, recursively, saveAbsolutePath,
224                     Yes.enableFilter, includedTypes);
225 }
226 
227 
228 /******************************************************************************
229  * Special type for files as entries in directories.
230  * Analogue of the 'dirent' structure type.
231  */
232 struct FileEntry {
233     /// Path to file.
234     string path;
235     /// Inode serial number.
236     ulong fileno;
237     /// UNIX file type. See: enum FileType.
238     FileType type;
239 
240     /// Creates an instance of a class from an existing file by its path.
241     this(string filepath) {
242         auto st = FileStat(filepath);
243         this.path = filepath;
244         this.fileno = st.ino;
245         this.type = st.type;
246     }
247 
248     /// Returns information about the properties of the file.
249     const FileStat getFileStat() {
250         return FileStat(this.path);
251     }
252 
253     /***************************************************************************
254      * Checks if the file is a symbolic link to a directory.
255      * The check is recursive (if the link points to a link).
256      */
257     bool isLinkToDir() {
258         if (type != FileType.symlink) {
259             return false;
260         }
261         string p;
262         try {
263             p = readLinkRecurse(this.path);
264         } catch (FileException e) {
265             return false;
266         }
267         return FileStat(p).type == FileType.directory;
268     }
269 }
270 
271 
272 /******************************************************************************
273  * Returns read symlink path. If the symlink points to other symlink,
274  * the function returns path of the last symlink of the link chain.
275  */
276 string readLinkRecurse(string link) {
277     size_t counter = 1;
278     auto path = link.readLink;
279     if (!path.exists) {
280         return path;
281     }
282     while(path.isSymlink) {
283         path = path.readLink;
284         counter++;
285         if (counter > eloopLimit) {
286             errno = ELOOP;
287             throw new FileException(path, errno);
288         }
289         if (!path.exists) {
290             // broken link
291             return path;
292         }
293     }
294     return path;
295 }
296 
297 
298 
299 /*******************************************************************************
300  * Checks if a file is a broken symbolic link is broken
301  * (whether indicates a non-existent location).
302  * Params:
303  *     link = Path to link.
304  *     recurse = Whether to check all nested symbolic links in a chain.
305  */
306 bool isBrokenSymlink(string link, Flag!"recurse" recurse = No.recurse)
307 do {
308     if (!link.isSymlink) {
309         return false;
310     }
311     if (recurse) {
312         string path = link.readLink;
313         size_t counter = 1;
314         while (path.exists) {
315             if (!path.isSymlink) {
316                 return false;
317             }
318             path = path.readLink;
319             counter++;
320             if (counter > eloopLimit) {
321                 errno = ELOOP;
322                 throw new FileException(path, errno);
323             }
324         }
325         return true;
326     }
327     return !link.readLink.exists;
328 }
329 
330 
331 /// Checks if a path is a pipe file (FIFO).
332 bool isPipe(in string path) {
333     return FileStat(path).type == FileType.pipe;
334 }
335 
336 
337 /// Checks if a path is a socket.
338 bool isSocket(in string path) {
339     return FileStat(path).type == FileType.socket;
340 }
341 
342 
343 /// Checks if a path is a symbolic link.
344 bool isSymlink(in string path) {
345     return FileStat(path).type == FileType.symlink;
346 }
347 
348 
349 /// Checks if a path is a regular file.
350 bool isRegularFile(in string path) {
351     return FileStat(path).type == FileType.regularFile;
352 }
353 /// Checks if a FileEntry object related to a regular file.
354 bool isRegularFile(const FileEntry entry) {
355     return entry.type == FileType.directory;
356 }
357 bool isRegularFile(ref const FileEntry entry) {
358     return entry.type == FileType.directory;
359 }
360 
361 
362 /// Checks if a path is a block device.
363 bool isBlockDevice(in string path) {
364     return FileStat(path).type == FileType.blockDevice;
365 }
366 
367 
368 /// Checks if a path is a directory.
369 bool isDir(in string path) {
370     return FileStat(path).type == FileType.directory;
371 }
372 /// Checks if a FileEntry object related to a directory.
373 bool isDir(const FileEntry entry) {
374     return entry.type == FileType.directory;
375 }
376 bool isDir(ref const FileEntry entry) {
377     return entry.type == FileType.directory;
378 }
379 
380 
381 
382 /***************************************************************************
383  * Checks if the file is a symbolic link to a directory.
384  * The check is recursive (if the link points to a link).
385  * Params:
386  *     path = path fo file
387  * Returns: whether a symbolic link is a link to a directory.
388  */
389 bool isSymlinkToDir(in string path) {
390     return FileEntry(path).isLinkToDir();
391 }
392 
393 
394 /// Checks if a path is a character device.
395 bool isCharDevice(in string path) {
396     return FileStat(path).type == FileType.charDevice;
397 }
398 
399 
400 /*******************************************************************************
401  * Special set of functions for work with ini-like .desktop files.
402  */
403 class DesktopEntry {
404 
405     /***************************************************************************
406      * This function adds new field to ini-like .desktop file.
407      * Params:
408      *     filepath = path to .desktop file
409      *     field = field name
410      *     value = value for new field
411      */
412     static void addField(string filepath, string field, string value) {
413         string content = std.file.readText(filepath);
414         auto newLine = field ~ "=" ~ value ~ "\n";
415         if (content.endsWith("\n")) {
416             content ~= newLine;
417         } else {
418             content ~= "\n" ~ newLine;
419         }
420         std.file.write(filepath, content);
421     }
422 
423     /***************************************************************************
424      * This function returns value of specified field from .desktop file.
425      * Params:
426      *     filepath = path to .desktop file
427      *     field = field name
428      */
429     static string getField(string filepath, string field) {
430         field = field.replace(`[`, `\[`).replace(`]`, `\]`);
431         string content = std.file.readText(filepath);
432         auto reg = regex(field ~ `=(.*)`);
433         auto res = matchFirst(content, reg);
434         return res[1].idup;
435     }
436 
437     /***************************************************************************
438      * This function sets new value for existent field from .desktop file.
439      * Params:
440      *     filepath = path to .desktop file
441      *     field = field name
442      *     value = new value of the field
443      */
444     static void setField(string filepath, string field, string value) {
445         string oldValue = DesktopEntry.getField(filepath, field);
446         string content = std.file.readText(filepath);
447         content = content.replace(field~"="~oldValue, field~"="~value);
448         std.file.write(filepath, content);
449     }
450 
451     /***************************************************************************
452      * The function returns title from .desktop file (field "Name").
453      */
454     static string getTitle(string pathToDesktopFile) {
455         string desktopEntry = readText(pathToDesktopFile);
456         auto indexOfNameField = desktopEntry.indexOf("Name=");
457         auto indexOfTypeField = desktopEntry.indexOf("\nType=");
458         return desktopEntry[indexOfNameField + 4 .. indexOfTypeField].idup;
459     }
460 
461     /***************************************************************************
462      * Creates a .desktop file with the link to the network resource.
463      * Params:
464      *     networkPath = path to the network resource
465      *     destinationDir = directory for saving
466      *     name = final desktop-file name (if empty, the name is taken from
467      *            the title of the HTML page or from network address)
468      * Returns: path to created desktop file
469      */
470     static string createNetworkLink(string networkPath,
471                                     string destinationDir,
472                                     string name="") {
473         import amalthea.net : isLinkToHTML, getHTMLPageTitle;
474         if (name.empty) {
475             bool HTML = isLinkToHTML(networkPath);
476             name = HTML ? getHTMLPageTitle(networkPath)
477                         : baseName(networkPath);
478             if (name.empty) {
479                 name = networkPath;
480             }
481         }
482         string desktopEntry = "[Desktop Entry]\n"
483                             ~ "Encoding=UTF-8\n"
484                             ~ "Name=" ~ name ~ "\n"
485                             ~ "Type=Link\n"
486                             ~ "URL=" ~ networkPath ~ "\n"
487                             ~ "Comment=" ~ "\n"
488                             ~ "Icon=text-html";
489         string finalFileName = name.replace("/", "|");
490         if (!endsWith(name, ".desktop")) {
491             finalFileName ~= ".desktop";
492         }
493         string filePath = chainPath(destinationDir, finalFileName).array;
494         std.file.write(filePath, desktopEntry);
495         return filePath;
496     }
497 
498     /***************************************************************************
499      * Returns link to network resource from .desktop file (field "URL").
500      */
501     static string getURL(string pathToDesktopFile) {
502         return DesktopEntry.getField(pathToDesktopFile, "URL");
503     }
504 }
505 
506 
507 /*******************************************************************************
508  * Returns absolute path for any raw path. Trailing slashes are removed.
509  */
510 string getAbsolutePath(string path) {
511     import std.path : buildNormalizedPath, absolutePath;
512     return buildNormalizedPath(absolutePath(path));
513 }
514 unittest {
515     assert(getAbsolutePath("/usr/bin") == "/usr/bin");
516     assert(getAbsolutePath("/usr/bin/") == "/usr/bin");
517 }
518 
519 
520 /*******************************************************************************
521  * Function for copying files and directories.
522  * Params:
523  *     from = source file
524  *     to   = destination
525  *     followLinks = whether to follow symbolic links
526  *     passErrors  = if No, the 'cp' function can throw exceptions
527  *                   else errors are ignored
528  *     printErrors = print information about errors or not
529  *                   (the parameter works if passErrors is Yes)
530  */
531 void cp(string from,
532         string to,
533         Flag!"followLinks" followLinks = No.followLinks,
534         Flag!"passErrors" passErrors = No.passErrors,
535         Flag!"printErrors" printErrors = Yes.printErrors) {
536 
537     void copyOneFile(string src, string dest) {
538         try {
539             if (dest.exists) {
540                 rm(dest);
541             }
542             alias isBroken = isBrokenSymlink;
543             if (!followLinks && src.isSymlink || isBroken(src, Yes.recurse)) {
544                 std.file.symlink(src.readLink, dest);
545             } else if (src.isPipe) {
546                 auto pipeStat = FileStat(src);
547                 amalthea.sys.createPipe(dest, pipeStat.mode);
548             } else {
549                 std.file.copy(src, dest);
550             }
551         } catch (Exception e) {
552             if (!passErrors) {
553                 throw e;
554             }
555             if (printErrors) {
556                 stderr.writeln(e.msg);
557             }
558         }
559     }
560 
561     void processSymlink(FileEntry entry, string dest) {
562         auto newPath = buildPath(dest, entry.path);
563         if (followLinks && entry.isLinkToDir) {
564             std.file.mkdirRecurse(newPath);
565             auto distantDir = entry.path.readLinkRecurse;
566             cp(distantDir, newPath, followLinks, passErrors, printErrors);
567         } else {
568             mkdirRecurse(dirName(newPath)); // create dir over file
569             copyOneFile(entry.path, newPath);
570         }
571     }
572 
573     void copyDirToDir(string src, string dest) {
574         to = getAbsolutePath(dest);
575         auto startPath = pwd;
576         cd(src);
577         scope(exit) cd(startPath);
578         FileEntry[] entries;
579         entries = getFiles(".", Yes.hidden, Yes.recursively, No.absPath);
580         foreach(entry; entries) {
581             if (entry.type == FileType.directory) {
582                 std.file.mkdirRecurse(buildPath(to, entry.path));
583             } else if (entry.type == FileType.symlink) {
584                 processSymlink(entry, to);
585             } else { // other files
586                 auto newPath = buildPath(to, entry.path);
587                 mkdirRecurse(dirName(newPath)); // create dir over file
588                 copyOneFile(entry.path, newPath);
589             }
590         }
591     }
592 
593     enforce(exists(from), new FileException(from, "Not found."));
594     if (from.isDir && to.exists && !to.isDir) {
595         throw new std.file.FileException(to, "Is not a directory.");
596     }
597 
598     if (from.isDir && !to.exists) {
599         std.file.mkdirRecurse(to);
600     }
601 
602     if (from.isDir && to.isDir) { // dir to dir
603         copyDirToDir(from, to);
604     } else if (!from.isDir && to.exists && to.isDir) { // file to dir
605         copyOneFile(from, buildPath(to, baseName(from)));
606     } else { //file to file
607         copyOneFile(from, to);
608     }
609 }
610 
611 
612 /// A low-level structure to filling with the 'statx' system call.
613 extern(C)
614 struct statx_t {
615    // 0x00
616    uint stx_mask;
617    uint stx_blksize;
618    ulong stx_attributes;
619    // 0x10
620    uint stx_nlink;
621    uint stx_uid;
622    uint stx_gid;
623    ushort stx_mode;
624    ushort[1] __spare0;
625    // 0x20
626    ulong stx_ino;
627    ulong stx_size;
628    ulong stx_blocks;
629    ulong stx_attributes_mask;
630    // 0x40
631    timestamp_t stx_atime;
632    timestamp_t stx_btime;
633    timestamp_t stx_ctime;
634    timestamp_t stx_mtime;
635    // 0x80
636    uint stx_rdev_major;
637    uint stx_rdev_minor;
638    uint stx_dev_major;
639    uint stx_dev_minor;
640    // 0x90
641    ulong stx_mnt_id;
642    ulong __spare2;
643    // 0xA0
644    ulong[12] __spare3;	// Spare space for future expansion
645 }
646 
647 
648 /*******************************************************************************
649  * This function  returns information about a file,
650  * storing it in the buffer pointed to by statxbuf.
651  * System call of Linux 4.11+, see more: `man statx`
652  * This function is used in the high-level FileStat structure type.
653  */
654 extern(C)
655 int statx(int dfd, const char* filename, int flags, uint mask, statx_t* stx);
656 
657 /// See: `man statx`
658 enum STATX_TYPE        = 0x00000001U;
659 enum STATX_MODE        = 0x00000002U;
660 enum STATX_NLINK       = 0x00000004U;
661 enum STATX_UID         = 0x00000008U;
662 enum STATX_GID         = 0x00000010U;
663 enum STATX_ATIME       = 0x00000020U;
664 enum STATX_MTIME       = 0x00000040U;
665 enum STATX_CTIME       = 0x00000080U;
666 enum STATX_INO         = 0x00000100U;
667 enum STATX_SIZE        = 0x00000200U;
668 enum STATX_BLOCKS      = 0x00000400U;
669 enum STATX_BASIC_STATS = 0x000007ffU;
670 enum STATX_BTIME       = 0x00000800U;
671 enum STATX_MNT_ID      = 0x00001000U;
672 
673 /// Mask for stat field by default.
674 enum STATX_DEFAULT = STATX_BASIC_STATS | STATX_BTIME;
675 
676 
677 /*******************************************************************************
678  * Returns file information as statx_t.
679  * Wrapper for low level statx call.
680  */
681 statx_t getStatX(string filepath) {
682     int atflag = AT_SYMLINK_NOFOLLOW;
683     statx_t stx;
684     char* p = cast(char*)toStringz(filepath);
685     auto ret = statx(AT_FDCWD, p, atflag, STATX_DEFAULT, &stx);
686     if (ret < 0) {
687         throw new FileException(filepath, getInfoAboutError(errno));
688     }
689     return stx;
690 }
691 
692 
693 /// High-level function for getting stat_t from file path.
694 stat_t getStat(string filepath) {
695     stat_t st;
696     int ret = lstat(filepath.toStringz, &st);
697     enforce(ret != -1, new FileException(filepath, errno));
698     return st;
699 }
700 
701 
702 /*******************************************************************************
703  * A structure for storing and retrieving file information on Linux.
704  */
705 struct FileStat {
706     /// Mask of bits indicating filled fields.
707     uint mask;
708     /// Block size for filesystem I/O.
709     uint blksize;
710     /// Extra file attribute indicators.
711     ulong attributes;
712     /// Number of hard links.
713     uint nlink;
714     /// User ID of owner.
715     uint uid;
716     /// Group ID of owner.
717     uint gid;
718     /// File type and mode.
719     ushort mode;
720     /// Inode number.
721     ulong ino;
722     /// Total size in bytes.
723     ulong size;
724     /// Number of 512B blocks allocated.
725     ulong blocks;
726     /// Mask to show what's supported in stx_attributes.
727     ulong attributes_mask;
728     /// Time of last access.
729     timestamp_t atime;
730     /// Time of creation.
731     timestamp_t btime;
732     /// Time of last modification.
733     timestamp_t mtime;
734     /// Time of last status change.
735     timestamp_t ctime;
736 
737     /// Device ID (if this file is a device).
738     ulong rdev;
739     /// Device major id (if this file is a device).
740     uint rdev_major;
741     /// Device minor id (if this file is a device).
742     uint rdev_minor;
743 
744     /// ID of device containing file.
745     ulong dev;
746     /// Major ID of the filesystem where the file resides.
747     uint dev_major;
748     /// Minor ID of the filesystem where the file resides.
749     uint dev_minor;
750 
751     /// File path.
752     string filepath;
753 
754     /// Stored mask for required structure fields.
755     uint fieldMask;
756 
757     /***************************************************************************
758      * The constructor creates object with file information by file path.
759      * Params:
760      *     filepath  = The path to the file being examined.
761      *     fieldMask = Used to tell the kernel which fields
762      *                 the caller is interested in.
763      */
764     this(string filepath, uint fieldMask = STATX_DEFAULT) {
765         this.filepath = filepath;
766         this.fieldMask = fieldMask;
767         update();
768     }
769 
770     /***************************************************************************
771      * This method re-reads and updates file information.
772      */
773     void update() {
774         enforce(
775             std.file.exists(filepath),
776             new FileException(filepath, "No such file or directory")
777         );
778         statx_t stx = getStatX(filepath);
779         this.mask = stx.stx_mask;
780         this.blksize = stx.stx_blksize;
781         this.attributes = stx.stx_attributes;
782         this.nlink = stx.stx_nlink;
783         this.uid = stx.stx_uid;
784         this.gid = stx.stx_gid;
785         this.mode = stx.stx_mode;
786         this.ino = stx.stx_ino;
787         this.size = stx.stx_size;
788         this.blocks = stx.stx_blocks;
789         this.attributes_mask = stx.stx_attributes_mask;
790         this.atime = stx.stx_atime;
791         this.btime = stx.stx_btime;
792         this.mtime = stx.stx_mtime;
793         this.ctime = stx.stx_ctime;
794         this.rdev_major = stx.stx_rdev_major;
795         this.rdev_minor = stx.stx_rdev_minor;
796         this.rdev = (cast(ulong)rdev_major << 8) | rdev_minor;
797         this.dev_major = stx.stx_dev_major;
798         this.dev_minor = stx.stx_dev_minor;
799         this.dev = (cast(ulong)dev_major << 8) | dev_minor;
800     }
801 
802     /***************************************************************************
803      * Gets user name of owner.
804      */
805     @property string user() {
806         return amalthea.sys.getNameByUID(uid);
807     }
808 
809     /***************************************************************************
810      * Gets group name of owner.
811      */
812     @property string group() {
813         return amalthea.sys.getNameByGID(gid);
814     }
815 
816     /***************************************************************************
817      * Returns file type.
818      */
819     @property const FileType type() {
820         return getFileTypeFromFileMode(this.mode);
821     }
822 }
823 
824 
825 /*******************************************************************************
826  * Gets FileType by stat field 'mode'.
827  */
828 FileType getFileTypeFromFileMode(uint mode) nothrow {
829     return getFileTypeFromFileMask(mode & S_IFMT);
830 }
831 
832 
833 /*******************************************************************************
834  * Gets FileType by the file mask obtained from stat field 'mode'.
835  */
836 FileType getFileTypeFromFileMask(uint mask) nothrow {
837     switch(mask) {
838         case S_IFSOCK: return FileType.socket;
839         case S_IFLNK:  return FileType.symlink;
840         case S_IFREG:  return FileType.regularFile;
841         case S_IFBLK:  return FileType.blockDevice;
842         case S_IFDIR:  return FileType.directory;
843         case S_IFCHR:  return FileType.charDevice;
844         case S_IFIFO:  return FileType.pipe;
845         default:       return FileType.unknown;
846     }
847 }
848 
849 /*******************************************************************************
850  * Enumeration of UNIX file types.
851  */
852 enum FileType {
853     socket = 's',
854     symlink = 'l',
855     regularFile = '-',
856     blockDevice = 'b',
857     directory = 'd',
858     charDevice = 'c',
859     pipe = 'p',
860     unknown = 'z'
861 }
862 
863 
864 /*******************************************************************************
865  * The function returns file permissions in a string form (like -rwxrwxrwx)
866  */
867 string makeUnixFileModeLine(in uint mode) {
868     char[10] permissions;
869     permissions[0] = cast(char)getFileTypeFromFileMode(mode);
870     foreach(shift, rwxMask; [0: S_IRWXU, 3: S_IRWXG, 6: S_IRWXO]) {
871         const rwxMode = (mode & rwxMask) >> (6-shift);
872         permissions[shift+1] = (rwxMode & 4) ? 'r' : '-';
873         permissions[shift+2] = (rwxMode & 2) ? 'w' : '-';
874         permissions[shift+3] = (rwxMode & 1) ? 'x' : '-';
875     }
876     if (mode & S_ISUID) {
877         permissions[3] = 's';
878     }
879     if (mode & S_ISGID) {
880         permissions[6] = 's';
881     }
882     if (mode & S_ISVTX) {
883         permissions[9] = 't';
884     }
885     return permissions.idup;
886 }
887 /// ditto
888 string makeUnixFileModeLine(const FileStat st) {
889     return makeUnixFileModeLine(st.mode);
890 }
891 string makeUnixFileModeLine(ref const FileStat st) {
892     return makeUnixFileModeLine(st.mode);
893 }
894 
895 
896 /*******************************************************************************
897  * Creates a new hard link (make a new name for a file, see `man 2 link`).
898  * Params:
899  *     original = Original file path.
900  *     link     = New link location path.
901  */
902 void hardlink(string original, string link) {
903     int ret = unix_unistd.link(original.toStringz, link.toStringz);
904     if (-1 == ret) {
905         string info = format!"Link from %s to %s"(original, link);
906         throw new FileException(info);
907     }
908 }
909 
910 private extern(C) {
911     int faccessat(
912         int dirfd, const char* pathname, int mode, int flags
913     ) nothrow @nogc;
914     enum AT_FDCWD = -100;
915     enum AT_SYMLINK_NOFOLLOW = 0x100;
916     enum F_OK = 0;
917 }
918 
919 /// Checks if the given file exists.
920 bool exists(in string filepath) nothrow {
921     return F_OK == faccessat(
922         AT_FDCWD, filepath.toStringz, 0, AT_SYMLINK_NOFOLLOW
923     );
924 }
925 
926 
927 /*******************************************************************************
928  * Function set for working with file extended attributes.
929  * See_Also: `man xattr`
930  */
931 class xattr {
932     /**
933      * Gets value of the required extended attribute.
934      * Params:
935      *     path = Path to explored file.
936      *            If it's a symlink, the target will be used.
937      *     key = Attribute name.
938      * Returns: read value.
939      */
940     static string read(string path, string key) {
941         auto pathPtr = path.toStringz;
942         auto keyPtr = key.toStringz;
943         auto valueLength = getxattr(pathPtr, keyPtr, null, 0);
944         enforce(valueLength != -1, new FileException("getxattr"));
945         char[] buffer = new char[valueLength];
946         valueLength = getxattr(pathPtr, keyPtr, buffer.ptr, valueLength);
947         enforce(valueLength != -1, new FileException("getxattr"));
948         return buffer.to!string;
949     }
950 
951     /**
952      * Sets new value of the required extended attribute.
953      * Params:
954      *     path = Path to explored file.
955      *            If it's a symlink, the target will be used.
956      *     key = Attribute name.
957      *           If the attribute doesn't exist, it will be created.
958      *     value = New value of the attribute.
959      */
960     static void set(string path, string key, string value) {
961         auto pathPtr = path.toStringz;
962         auto keyPtr = key.toStringz;
963         auto valuePtr = cast(void*)(value.toStringz);
964         int ret = setxattr(pathPtr, keyPtr, valuePtr, value.length, 0);
965         enforce(ret != -1, new FileException("setxattr"));
966     }
967 
968     /**
969      * Removes the requested attribute.
970      * Params:
971      *     path = Path to file.
972      *            If it's a symlink, the target will be used.
973      *     key = Attribute name.
974      */
975     static void remove(string path, string key) {
976         int ret = removexattr(path.toStringz, key.toStringz);
977         enforce(ret != -1, new FileException("removexattr"));
978     }
979 
980     /**
981      * Reads list of all attributes.
982      * Params:
983      *     path = Path to explored file.
984      *            If it's a symlink, the target will be used.
985      * Returns: extended attributes of the specified file.
986      */
987     static string[] getAttrs(string path) {
988         auto pathPtr = path.toStringz;
989         auto bufferLength = listxattr(pathPtr, null, 0);
990         enforce(bufferLength != -1, new FileException("listxattr"));
991         ubyte[] buffer = new ubyte[bufferLength];
992         bufferLength = listxattr(pathPtr, cast(char*)buffer.ptr, bufferLength);
993         enforce(bufferLength != -1, new FileException("listxattr"));
994         if (bufferLength == 0) {
995             return [];
996         }
997         buffer = buffer[0 .. $-1];  // without last zero
998         string[] list;
999         foreach(ubyte[] elem; buffer.splitter(0)) {
1000             list ~= to!string(cast(char[])elem);
1001         }
1002         return list;
1003     }
1004 
1005     /**
1006      * Creates associative array with attribute names and attribute values.
1007      * Params:
1008      *     path = Path to explored file.
1009      *            If it's a symlink, the target will be used.
1010      * Returns: all extended attributes and their values of the specified file.
1011      */
1012     static string[string] getAttrsAndValues(string path) {
1013         string[] names = xattr.getAttrs(path);
1014         string[string] result;
1015         foreach(attr; names) {
1016             result[attr] = xattr.read(path, attr);
1017         }
1018         return result;
1019     }
1020 }
1021 
1022 
1023 /*******************************************************************************
1024  * Truncate a file to a specified length.
1025  * Params:
1026  *     filepath = File to be resized.
1027  *     size = New length in bytes.
1028  */
1029 void truncate(string filepath, size_t size) {
1030     int ret = unix_unistd.truncate(filepath.toStringz, size);
1031     enforce(ret != -1, new FileException(filepath, errno));
1032 }
1033 
1034 
1035 /******************************************************************************
1036  * Create a directory only if it doesn't exist.
1037  */
1038 void safeMkdir(string path) {
1039     errno = 0;
1040     int ret = unix_stat.mkdir(path.toStringz, oct!777);
1041     if (ret == -1 && errno == EEXIST) {
1042         return;
1043     }
1044     throw new FileException("safeMkdir");
1045 }
1046 
1047 
1048 private extern(C)
1049 int posix_fadvise(int fd, off_t offset, off_t len, int advice) nothrow @nogc;
1050 
1051 
1052 /// Access patterns for file data.
1053 enum FileAdvice {
1054     normal     = 0,  /// No further special treatment.
1055     random     = 1,  /// Expect random page references.
1056     sequential = 2,  /// Expect sequential page references.
1057     willNeed   = 3,  /// Will need these pages.
1058     dontNeed   = 4,  /// Don't need these pages.
1059     noReuse    = 5   /// Data will be accessed once.
1060 }
1061 
1062 
1063 /*******************************************************************************
1064  * Predeclare an access pattern for file data.
1065  * Params:
1066  *     f = File object.
1067  *     advice = Access patern.
1068  *
1069  * See_Also: `man 2 posix_fadvise`
1070  */
1071 void applyFileAdvise(File f, FileAdvice advice) {
1072     int ret = posix_fadvise(f.fileno, 0, 0, cast(int)advice);
1073     enforce(ret == 0, new FileException("applyFileAdvise"));
1074 }