1 /**
2  * Moving files and directories to trash can.
3  * Copyright:
4  *  Roman Chistokhodov, 2016
5  * License:
6  *  $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
7  */
8 
9 module trashcan;
10 
11 import std.path;
12 import std.string;
13 import std.file;
14 import std.exception;
15 import std.range : InputRange, inputRangeObject;
16 import std.datetime;
17 
18 import isfreedesktop;
19 
20 static if (isFreedesktop) {
21     import std.uri : encode, decode;
22     import volumeinfo;
23     import xdgpaths : xdgDataHome, xdgAllDataDirs;
24 }
25 
26 /**
27  * Flags to rule the trashing behavior.
28  *
29  * $(BLUE Valid only for freedesktop environments).
30  *
31  * See_Also: $(LINK2 https://specifications.freedesktop.org/trash-spec/trashspec-1.0.html, Trash specification).
32  */
33 enum TrashOptions : int
34 {
35     /**
36      * No options. Just move file to user home trash directory
37      * not paying attention to partition where file resides.
38      */
39     none = 0,
40     /**
41      * If file that needs to be deleted resides on non-home partition
42      * and top trash directory ($topdir/.Trash/$uid) failed some check,
43      * fallback to user top trash directory ($topdir/.Trash-$uid).
44      *
45      * Makes sense only in conjunction with $(D useTopDirs).
46      */
47     fallbackToUserDir = 1,
48     /**
49      * If file that needs to be deleted resides on non-home partition
50      * and checks for top trash directories failed,
51      * fallback to home trash directory.
52      *
53      * Makes sense only in conjunction with $(D useTopDirs).
54      */
55     fallbackToHomeDir = 2,
56 
57     /**
58      * Whether to use top trash directories at all.
59      *
60      * If no $(D fallbackToUserDir) nor $(D fallbackToHomeDir) flags are set,
61      * and file that needs to be deleted resides on non-home partition,
62      * and top trash directory ($topdir/.Trash/$uid) failed some check,
63      * exception will be thrown. This can be used to report errors to administrator or user.
64      */
65     useTopDirs = 4,
66 
67     /**
68      * Whether to check presence of 'sticky bit' on $topdir/.Trash directory.
69      *
70      * Makes sense only in conjunction with $(D useTopDirs).
71      */
72     checkStickyBit = 8,
73 
74     /**
75      * All flags set.
76      */
77     all = (TrashOptions.fallbackToUserDir | TrashOptions.fallbackToHomeDir | TrashOptions.checkStickyBit | TrashOptions.useTopDirs)
78 }
79 
80 static if (isFreedesktop)
81 {
82 private:
83     import std.format : format;
84 
85     @trusted string numberedBaseName(scope string path, uint number) {
86         return format("%s %s%s", path.baseName.stripExtension, number, path.extension);
87     }
88 
89     unittest
90     {
91         assert(numberedBaseName("/root/file.ext", 1) == "file 1.ext");
92         assert(numberedBaseName("/root/file", 2) == "file 2");
93     }
94 
95     @trusted string ensureDirExists(scope string dir) {
96         std.file.mkdirRecurse(dir);
97         return dir;
98     }
99 
100     import core.sys.posix.sys.types;
101     import core.sys.posix.sys.stat;
102     import core.sys.posix.unistd;
103     import core.sys.posix.fcntl;
104 
105     @trusted string topDir(string path)
106     in {
107         assert(path.isAbsolute);
108     }
109     do {
110         return volumePath(path);
111     }
112 
113     void checkDiskTrashMode(mode_t mode, const bool checkStickyBit = true)
114     {
115         enforce(!S_ISLNK(mode), "Top trash directory is a symbolic link");
116         enforce(S_ISDIR(mode), "Top trash path is not a directory");
117         if (checkStickyBit) {
118             enforce((mode & S_ISVTX) != 0, "Top trash directory does not have sticky bit");
119         }
120     }
121 
122     unittest
123     {
124         assertThrown(checkDiskTrashMode(S_IFLNK|S_ISVTX));
125         assertThrown(checkDiskTrashMode(S_IFDIR));
126         assertNotThrown(checkDiskTrashMode(S_IFDIR|S_ISVTX));
127         assertNotThrown(checkDiskTrashMode(S_IFDIR, false));
128     }
129 
130     @trusted string checkDiskTrash(scope string topdir, const bool checkStickyBit = true)
131     in {
132         assert(topdir.length);
133     }
134     body {
135         string trashDir = buildPath(topdir, ".Trash");
136         stat_t trashStat;
137         enforce(lstat(trashDir.toStringz, &trashStat) == 0, "Top trash directory does not exist");
138         checkDiskTrashMode(trashStat.st_mode, checkStickyBit);
139         return trashDir;
140     }
141 
142     @safe string userTrashSubdir(scope string trashDir, uid_t uid) {
143         import std.conv : to;
144         return buildPath(trashDir, uid.to!string);
145     }
146 
147     unittest
148     {
149         assert(userTrashSubdir("/.Trash", 600) == buildPath("/.Trash", "600"));
150     }
151 
152     @trusted string ensureUserTrashSubdir(scope string trashDir)
153     {
154         return userTrashSubdir(trashDir, getuid()).ensureDirExists();
155     }
156 
157     @safe string userTrashDir(string topdir, uid_t uid) {
158         return buildPath(topdir, format(".Trash-%s", uid));
159     }
160 
161     unittest
162     {
163         assert(userTrashDir("/topdir", 700) == buildPath("/topdir", ".Trash-700"));
164     }
165 
166     @trusted string ensureUserTrashDir(scope string topdir)
167     {
168         return userTrashDir(topdir, getuid()).ensureDirExists();
169     }
170 }
171 
172 version(OSX)
173 {
174 private:
175     import core.sys.posix.dlfcn;
176 
177     struct FSRef {
178         char[80] hidden;
179     };
180 
181     alias ubyte Boolean;
182     alias int OSStatus;
183     alias uint OptionBits;
184 
185     extern(C) @nogc @system OSStatus _dummy_FSPathMakeRefWithOptions(const(char)* path, OptionBits, FSRef*, Boolean*) nothrow {return 0;}
186     extern(C) @nogc @system OSStatus _dummy_FSMoveObjectToTrashSync(const(FSRef)*, FSRef*, OptionBits) nothrow {return 0;}
187 }
188 
189 /**
190  * Move file or directory to trash can.
191  * Params:
192  *  path = Path of item to remove. Must be absolute.
193  *  options = Control behavior of trashing on freedesktop environments.
194  * Throws:
195  *  $(B Exception) when given path is not absolute or does not exist, or some error occured during operation,
196  *  or the operation is not supported on the current platform.
197  */
198 @trusted void moveToTrash(scope string path, TrashOptions options = TrashOptions.all)
199 {
200     if (!path.isAbsolute) {
201         throw new Exception("Path must be absolute");
202     }
203     if (!path.exists) {
204         throw new Exception("Path does not exist");
205     }
206 
207     version(Windows) {
208         import core.sys.windows.shellapi;
209         import core.sys.windows.winbase;
210         import core.stdc.wchar_;
211         import std.windows.syserror;
212         import std.utf;
213 
214         SHFILEOPSTRUCTW fileOp;
215         fileOp.wFunc = FO_DELETE;
216         fileOp.fFlags = FOF_SILENT | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_NOCONFIRMMKDIR | FOF_ALLOWUNDO;
217         auto wFileName = (path ~ "\0\0").toUTF16();
218         fileOp.pFrom = wFileName.ptr;
219         int r = SHFileOperation(&fileOp);
220         if (r != 0) {
221             import std.format;
222             throw new Exception(format("SHFileOperation failed with error code %d", r));
223         }
224     } else version(OSX) {
225         void* handle = dlopen("CoreServices.framework/Versions/A/CoreServices", RTLD_NOW | RTLD_LOCAL);
226         if (handle !is null) {
227             scope(exit) dlclose(handle);
228 
229             auto ptrFSPathMakeRefWithOptions = cast(typeof(&_dummy_FSPathMakeRefWithOptions))dlsym(handle, "FSPathMakeRefWithOptions");
230             if (ptrFSPathMakeRefWithOptions is null) {
231                 throw new Exception(fromStringz(dlerror()).idup);
232             }
233 
234             auto ptrFSMoveObjectToTrashSync = cast(typeof(&_dummy_FSMoveObjectToTrashSync))dlsym(handle, "FSMoveObjectToTrashSync");
235             if (ptrFSMoveObjectToTrashSync is null) {
236                 throw new Exception(fromStringz(dlerror()).idup);
237             }
238 
239             FSRef source;
240             enforce(ptrFSPathMakeRefWithOptions(toStringz(path), 1, &source, null) == 0, "Could not make FSRef from path");
241             FSRef target;
242             enforce(ptrFSMoveObjectToTrashSync(&source, &target, 0) == 0, "Could not move path to trash");
243         } else {
244             throw new Exception(fromStringz(dlerror()).idup);
245         }
246     } else {
247         static if (isFreedesktop) {
248             string dataPath = xdgDataHome(null, true);
249             if (!dataPath.length) {
250                 throw new Exception("Could not access data folder");
251             }
252             dataPath = dataPath.absolutePath;
253 
254             string trashBasePath;
255             bool usingTopdir = false;
256             string fileTopDir;
257 
258             if ((options & TrashOptions.useTopDirs) != 0) {
259                 string dataTopDir = topDir(dataPath);
260                 fileTopDir = topDir(path);
261 
262                 enforce(fileTopDir.length, "Could not get topdir of file being trashed");
263                 enforce(dataTopDir.length, "Could not get topdir of home data directory");
264 
265                 if (dataTopDir != fileTopDir) {
266                     try {
267                         string diskTrash = checkDiskTrash(fileTopDir, (options & TrashOptions.checkStickyBit) != 0);
268                         trashBasePath = ensureUserTrashSubdir(diskTrash);
269                         usingTopdir = true;
270                     } catch(Exception e) {
271                         try {
272                             if ((options & TrashOptions.fallbackToUserDir) != 0) {
273                                 trashBasePath = ensureUserTrashDir(fileTopDir);
274                                 usingTopdir = true;
275                             } else {
276                                 throw e;
277                             }
278                         } catch(Exception e) {
279                             if (!(options & TrashOptions.fallbackToHomeDir)) {
280                                 throw e;
281                             }
282                         }
283                     }
284                 }
285             }
286 
287             if (trashBasePath is null) {
288                 trashBasePath = ensureDirExists(buildPath(dataPath, "Trash"));
289             }
290             enforce(trashBasePath.length, "Could not access base trash folder");
291 
292             string trashInfoDir = ensureDirExists(buildPath(trashBasePath, "info"));
293             string trashFilePathsDir = ensureDirExists(buildPath(trashBasePath, "files"));
294 
295             string trashInfoPath = buildPath(trashInfoDir, path.baseName ~ ".trashinfo");
296             string trashFilePath = buildPath(trashFilePathsDir, path.baseName);
297 
298             import std.conv : octal;
299             import core.stdc.errno;
300 
301             auto currentTime = Clock.currTime;
302             currentTime.fracSecs = Duration.zero;
303             string timeString = currentTime.toISOExtString();
304             string contents = format("[Trash Info]\nPath=%s\nDeletionDate=%s\n", (usingTopdir ? path.relativePath(fileTopDir) : path).encode(), timeString);
305 
306             const mode = O_CREAT | O_WRONLY | O_EXCL;
307             int fd;
308             uint number = 1;
309             while(trashFilePath.exists || ((fd = .open(toStringz(trashInfoPath), mode, octal!666)) == -1 && errno == EEXIST)) {
310                 string baseName = numberedBaseName(path, number);
311                 trashFilePath = buildPath(trashFilePathsDir, baseName);
312                 trashInfoPath = buildPath(trashInfoDir, baseName ~ ".trashinfo");
313                 number++;
314             }
315             errnoEnforce(fd != -1);
316             scope(exit) .close(fd);
317             errnoEnforce(write(fd, contents.ptr, contents.length) == contents.length);
318 
319             path.rename(trashFilePath);
320         } else {
321             throw new Exception("Trashing operation is not implemented on this platform");
322         }
323     }
324 }
325 
326 unittest
327 {
328     assertThrown(moveToTrash("notabsolute"));
329 }
330 
331 version(Windows)
332 {
333     import std.typecons : RefCounted, refCounted, RefCountedAutoInitialize;
334     import std.windows.syserror : WindowsException;
335 
336     import core.sys.windows.windows;
337     import core.sys.windows.shlobj;
338     import core.sys.windows.shlwapi;
339     import core.sys.windows.wtypes;
340     import core.sys.windows.oaidl;
341     import core.sys.windows.objidl;
342 
343     pragma(lib, "Ole32");
344     pragma(lib, "OleAut32");
345 }
346 
347 version(Windows) private struct ItemIdList
348 {
349     @disable this(this);
350     this(LPITEMIDLIST pidl) {
351         this.pidl = pidl;
352     }
353     alias pidl this;
354     LPITEMIDLIST pidl;
355     ~this() {
356         if (pidl) {
357             CoTaskMemFree(pidl);
358             pidl = null;
359         }
360     }
361 }
362 
363 /// Item (file or folder) stored in the trashcan.
364 struct TrashcanItem
365 {
366     version(Windows) private @trusted this(string restorePath, bool isDir, ref scope const SysTime deletionTime, LPITEMIDLIST pidl) {
367         _restorePath = restorePath;
368         _isDir = isDir;
369         _deletionTime = deletionTime;
370         this.pidl = refCounted(ItemIdList(pidl));
371     }
372     static if (isFreedesktop) {
373         private @trusted this(string restorePath, bool isDir, ref scope const SysTime deletionTime, string trashInfoPath, string trashedPath) {
374             assert(trashInfoPath.length != 0);
375             assert(trashedPath.length != 0);
376             _restorePath = restorePath;
377             _isDir = isDir;
378             _deletionTime = deletionTime;
379             _trashInfoPath = trashInfoPath;
380             _trashedPath = trashedPath;
381         }
382     }
383     /// Original location of the item (absolute path) before it was moved to trashcan.
384     @safe @property @nogc nothrow pure string restorePath() const {
385         return _restorePath;
386     }
387     /// Whether the item is directory.
388     @safe @property @nogc nothrow pure  bool isDir() const {
389         return _isDir;
390     }
391     /// The time when the item was moved to trashcan.
392     @safe @property @nogc nothrow pure SysTime deletionTime() const {
393         return _deletionTime;
394     }
395     version(D_Ddoc) {
396         static if (!is(typeof(LPITEMIDLIST.init)))
397         {
398             static struct LPITEMIDLIST {}
399         }
400         /**
401          * Windows-specific function to get LPITEMIDLIST associated with item.
402          *
403          * Note:
404          *  The returned object must not outlive this TrashcanItem (or its copies). If you want to keep this object around use $(LINK2 https://msdn.microsoft.com/en-us/library/windows/desktop/bb776433(v=vs.85).aspx, ILClone). Don't forget to call ILFree or CoTaskMemFree, when it's no longer needed.
405          */
406         @system @property @nogc nothrow LPITEMIDLIST itemIdList() {return LPITEMIDLIST.init;}
407         /**
408          * Freedesktop-specific function to get .trashinfo file path.
409          */
410         @property @nogc nothrow string trashInfoPath() const {return string.init;}
411         /**
412          * Freedesktop-specific function to get the path where the trashed file or directory is located.
413          */
414         @property @nogc nothrow string trashedPath() const {return string.init;}
415     } else version(Windows) {
416         @system @property @nogc nothrow LPITEMIDLIST itemIdList() {
417             if (pidl.refCountedStore.isInitialized)
418                 return pidl.refCountedPayload.pidl;
419             return null;
420         }
421     } else static if (isFreedesktop) {
422         @safe @property @nogc nothrow string trashInfoPath() const {
423             return _trashInfoPath;
424         }
425         @safe @property @nogc nothrow string trashedPath() const {
426             return _trashedPath;
427         }
428     }
429 private:
430     string _restorePath;
431     bool _isDir;
432     SysTime _deletionTime;
433     version(Windows) RefCounted!(ItemIdList, RefCountedAutoInitialize.no) pidl;
434     static if (isFreedesktop) {
435         string _trashInfoPath;
436         string _trashedPath;
437     }
438 }
439 
440 version(Windows) private
441 {
442     // Redefine IShellFolder2 since it's bugged in druntime
443     interface IShellFolder2 : IShellFolder
444     {
445         HRESULT GetDefaultSearchGUID(GUID*);
446         HRESULT EnumSearches(IEnumExtraSearch*);
447         HRESULT GetDefaultColumn(DWORD, ULONG*, ULONG*);
448         HRESULT GetDefaultColumnState(UINT, SHCOLSTATEF*);
449         HRESULT GetDetailsEx(LPCITEMIDLIST, const(SHCOLUMNID)*, VARIANT*);
450         HRESULT GetDetailsOf(LPCITEMIDLIST, UINT, SHELLDETAILS*);
451         HRESULT MapColumnToSCID(UINT, SHCOLUMNID*);
452     }
453 
454     // Define missing declarations
455     alias SICHINTF = DWORD;
456 
457     enum SIGDN {
458       SIGDN_NORMALDISPLAY,
459       SIGDN_PARENTRELATIVEPARSING,
460       SIGDN_DESKTOPABSOLUTEPARSING,
461       SIGDN_PARENTRELATIVEEDITING,
462       SIGDN_DESKTOPABSOLUTEEDITING,
463       SIGDN_FILESYSPATH,
464       SIGDN_URL,
465       SIGDN_PARENTRELATIVEFORADDRESSBAR,
466       SIGDN_PARENTRELATIVE,
467       SIGDN_PARENTRELATIVEFORUI
468     };
469 
470     interface IShellItem : IUnknown {
471         HRESULT BindToHandler(IBindCtx pbc, REFGUID bhid, REFIID riid, void **ppv);
472         HRESULT GetParent(IShellItem *ppsi);
473         HRESULT GetDisplayName(SIGDN  sigdnName, LPWSTR *ppszName);
474         HRESULT GetAttributes(SFGAOF sfgaoMask, SFGAOF *psfgaoAttribs);
475         HRESULT Compare(IShellItem psi, SICHINTF hint, int *piOrder);
476     }
477 
478     extern(Windows) HRESULT SHCreateShellItem(LPCITEMIDLIST pidlParent, IShellFolder psfParent, LPCITEMIDLIST pidl, IShellItem *ppsi) nothrow @nogc;
479     extern(Windows) LPITEMIDLIST ILCreateFromPath(PCTSTR pszPath);
480 
481     alias IFileOperationProgressSink = IUnknown;
482     alias IOperationsProgressDialog = IUnknown;
483     alias IPropertyChangeArray = IUnknown;
484 
485     immutable CLSID CLSID_FileOperation = {0x3ad05575,0x8857,0x4850,[0x92,0x77,0x11,0xb8,0x5b,0xdb,0x8e,0x9]};
486     immutable IID IID_IFileOperation = {0x947aab5f,0xa5c,0x4c13,[0xb4,0xd6,0x4b,0xf7,0x83,0x6f,0xc9,0xf8]};
487 
488     interface IFileOperation : IUnknown
489     {
490         HRESULT Advise(IFileOperationProgressSink pfops, DWORD *pdwCookie);
491         HRESULT Unadvise(DWORD dwCookie);
492         HRESULT SetOperationFlags(DWORD dwOperationFlags);
493         HRESULT SetProgressMessage(LPCWSTR pszMessage);
494         HRESULT SetProgressDialog(IOperationsProgressDialog popd);
495         HRESULT SetProperties (IPropertyChangeArray pproparray);
496         HRESULT SetOwnerWindow(HWND hwndOwner);
497         HRESULT ApplyPropertiesToItem(IShellItem psiItem);
498         HRESULT ApplyPropertiesToItems (IUnknown punkItems);
499         HRESULT RenameItem(IShellItem psiItem, LPCWSTR pszNewName, IFileOperationProgressSink pfopsItem);
500         HRESULT RenameItems(IUnknown pUnkItems, LPCWSTR pszNewName);
501         HRESULT MoveItem(IShellItem psiItem, IShellItem psiDestinationFolder, LPCWSTR pszNewName, IFileOperationProgressSink pfopsItem);
502         HRESULT MoveItems(IUnknown punkItems, IShellItem psiDestinationFolder);
503         HRESULT CopyItem(IShellItem psiItem, IShellItem psiDestinationFolder, LPCWSTR pszCopyName, IFileOperationProgressSink pfopsItem);
504         HRESULT CopyItems(IUnknown punkItems, IShellItem psiDestinationFolder);
505         HRESULT DeleteItem(IShellItem psiItem, IFileOperationProgressSink pfopsItem);
506         HRESULT DeleteItems(IUnknown punkItems);
507         HRESULT NewItem(IShellItem psiDestinationFolder, DWORD dwFileAttributes, LPCWSTR pszName, LPCWSTR pszTemplateName, IFileOperationProgressSink pfopsItem);
508         HRESULT PerformOperations();
509         HRESULT GetAnyOperationsAborted(BOOL *pfAnyOperationsAborted);
510     }
511 
512     static @trusted string StrRetToString(ref scope STRRET strRet, LPITEMIDLIST pidl)
513     {
514         import std.string : fromStringz;
515         switch (strRet.uType)
516         {
517         case STRRET_CSTR:
518             return fromStringz(strRet.cStr.ptr).idup;
519         case STRRET_OFFSET:
520             return string.init;
521         case STRRET_WSTR:
522             char[MAX_PATH] szTemp;
523             auto len = WideCharToMultiByte (CP_UTF8, 0, strRet.pOleStr, -1, szTemp.ptr, szTemp.sizeof, null, null);
524             scope(exit) CoTaskMemFree(strRet.pOleStr);
525             if (len)
526                 return szTemp[0..len-1].idup;
527             else
528                 return string.init;
529         default:
530             return string.init;
531         }
532     }
533 
534     static @trusted wstring StrRetToWString(ref scope STRRET strRet, LPITEMIDLIST pidl)
535     {
536         switch (strRet.uType)
537         {
538         case STRRET_CSTR:
539         {
540             char[] cstr = fromStringz(strRet.cStr.ptr);
541             wchar[] toReturn;
542             toReturn.reserve(cstr.length);
543             foreach(char c; cstr)
544                 toReturn ~= cast(wchar)c;
545             return assumeUnique(toReturn);
546         }
547         case STRRET_WSTR:
548             scope(exit) CoTaskMemFree(strRet.pOleStr);
549             return strRet.pOleStr[0..lstrlenW(strRet.pOleStr)].idup;
550         default:
551             return wstring.init;
552         }
553     }
554 
555     static @trusted SysTime StrRetToSysTime(ref scope STRRET strRet, LPITEMIDLIST pidl)
556     {
557         auto str = StrRetToWString(strRet, pidl);
558         if (str.length) {
559             wchar[] temp;
560             temp.reserve(str.length + 1);
561             foreach(wchar c; str) {
562                 if (c != '\u200E' && c != '\u200F')
563                     temp ~= c;
564             }
565             temp ~= '\0';
566             DATE date;
567             if(SUCCEEDED(VarDateFromStr(temp.ptr, LOCALE_USER_DEFAULT, 0, &date)))
568             {
569                 SYSTEMTIME sysTime;
570                 if (VariantTimeToSystemTime(date, &sysTime))
571                     return SYSTEMTIMEToSysTime(&sysTime);
572             }
573         }
574         return SysTime.init;
575     }
576 
577     @trusted static void henforce(HRESULT hres, lazy string msg = null, string file = __FILE__, size_t line = __LINE__)
578     {
579         if (FAILED(hres))
580             throw new WindowsException(hres, msg, file, line);
581     }
582 
583     @trusted static string getDisplayNameOf(IShellFolder folder, LPITEMIDLIST pidl)
584     in {
585         assert(folder);
586         assert(pidl);
587     }
588     do {
589         STRRET strRet;
590         if (SUCCEEDED(folder.GetDisplayNameOf(pidl, SHGNO.SHGDN_NORMAL, &strRet)))
591             return StrRetToString(strRet, pidl);
592         return string.init;
593     }
594 
595     @trusted static string getStringDetailOf(IShellFolder2 folder, LPITEMIDLIST pidl, uint index)
596     in {
597         assert(folder);
598         assert(pidl);
599     }
600     do {
601         SHELLDETAILS details;
602         if(SUCCEEDED(folder.GetDetailsOf(pidl, index, &details)))
603             return StrRetToString(details.str, pidl);
604         return string.init;
605     }
606 
607     @trusted static wstring getWStringDetailOf(IShellFolder2 folder, LPITEMIDLIST pidl, uint index)
608     in {
609         assert(folder);
610         assert(pidl);
611     }
612     do {
613         SHELLDETAILS details;
614         if(SUCCEEDED(folder.GetDetailsOf(pidl, index, &details)))
615             return StrRetToWString(details.str, pidl);
616         return wstring.init;
617     }
618 
619     @trusted static SysTime getSysTimeDetailOf(IShellFolder2 folder, LPITEMIDLIST pidl, uint index)
620     in {
621         assert(folder);
622         assert(pidl);
623     }
624     do {
625         SHELLDETAILS details;
626         if(SUCCEEDED(folder.GetDetailsOf(pidl, index, &details)))
627             return StrRetToSysTime(details.str, pidl);
628         return SysTime.init;
629     }
630 
631     @trusted static void RunVerb(string verb)(IShellFolder folder, LPITEMIDLIST pidl)
632     in {
633         assert(folder);
634     }
635     do {
636         enforce(pidl !is null, "Empty trashcan item, can't run an operation");
637         IContextMenu contextMenu;
638         henforce(folder.GetUIObjectOf(null, 1, cast(LPCITEMIDLIST*)(&pidl), &IID_IContextMenu, null, cast(LPVOID *)&contextMenu), "Failed to get context menu ui object");
639         assert(pidl);
640         assert(contextMenu);
641         scope(exit) contextMenu.Release();
642         CMINVOKECOMMANDINFO ci;
643         ci.fMask = CMIC_MASK_FLAG_NO_UI;
644         ci.cbSize = CMINVOKECOMMANDINFO.sizeof;
645         ci.lpVerb  = verb;
646         henforce(contextMenu.InvokeCommand(&ci), "Failed to " ~ verb ~ " item");
647     }
648 
649     @trusted static IFileOperation CreateFileOperation()
650     {
651         IFileOperation op;
652         henforce(CoCreateInstance(&CLSID_FileOperation, null, CLSCTX_ALL, &IID_IFileOperation, cast(void**)&op), "Failed to create instance of IFileOperation");
653         assert(op);
654         return op;
655     }
656 
657     @trusted static IShellItem CreateShellItem(IShellFolder folder, LPITEMIDLIST pidl)
658     {
659         IShellItem item;
660         henforce(SHCreateShellItem(null, folder, pidl, &item), "Failed to get IShellItem");
661         assert(item);
662         return item;
663     }
664 
665     @trusted static void RunDeleteOperation(IShellFolder folder, LPITEMIDLIST pidl)
666     in {
667         assert(folder);
668     }
669     body {
670         enforce(pidl !is null, "Empty trashcan item, can't run a delete operation");
671         IShellItem item = CreateShellItem(folder, pidl);
672         scope(exit) item.Release();
673 
674         IFileOperation op = CreateFileOperation();
675         scope(exit) op.Release();
676 
677         op.SetOperationFlags(FOF_NOCONFIRMATION|FOF_NOERRORUI|FOF_SILENT);
678         op.DeleteItem(item, null);
679         henforce(op.PerformOperations(), "Failed to perform file deletion operation");
680     }
681 
682     @trusted static void RunRestoreOperation(IShellFolder2 folder, LPITEMIDLIST pidl)
683     in {
684         assert(folder);
685     }
686     body {
687         enforce(pidl !is null, "Empty trashcan item, can't run a restore operation");
688 
689         import std.utf;
690         wstring originalLocation = getWStringDetailOf(folder, pidl, 1);
691         auto originalLocationZ = originalLocation.toUTF16z;
692 
693         auto originalLocationPidl = ILCreateFromPath(originalLocationZ);
694         scope(exit) ILFree(originalLocationPidl);
695 
696         IShellItem originalLocationItem = CreateShellItem(null, originalLocationPidl);
697 
698         IShellItem item = CreateShellItem(folder, pidl);
699         scope(exit) item.Release();
700 
701         IFileOperation op = CreateFileOperation();
702         scope(exit) op.Release();
703 
704         op.SetOperationFlags(FOF_NOCONFIRMATION|FOF_NOERRORUI|FOF_SILENT);
705         op.MoveItem(item, originalLocationItem, null, null);
706         henforce(op.PerformOperations(), "Failed to perform file deletion operation");
707     }
708 }
709 
710 /// Interface to trashcan.
711 interface ITrashcan
712 {
713     /// List items stored in trashcan.
714     @trusted InputRange!TrashcanItem byItem();
715     /// Restore item to its original location.
716     @safe void restore(ref scope TrashcanItem item);
717     /// Ditto
718     @trusted final void restore(TrashcanItem item) {
719         restore(item);
720     }
721     /// Erase item from trashcan.
722     @safe void erase(ref scope TrashcanItem item);
723     /// Ditto
724     @trusted final erase(TrashcanItem item) {
725         erase(item);
726     }
727     /// The name of trashcan (possibly localized).
728     @property @safe string displayName() nothrow;
729 }
730 
731 version(D_Ddoc)
732 {
733     /**
734      * Implementation of $(D ITrashcan). This class may have additional platform-dependent functions and different constructors.
735      * This class is currently available only for $(BLUE Windows) and $(BLUE Freedesktop) (GNU/Linux, FreeBSD, etc.) platforms.
736      */
737     final class Trashcan : ITrashcan
738     {
739         ///
740         @trusted this() {}
741         /// Lazily list items stored in trashcan.
742         @trusted InputRange!TrashcanItem byItem() {return null;}
743         /**
744          * Restore item to its original location.
745          * Throws:
746          *  $(B WindowsException) on Windows when the operation failed.$(BR)
747          *  $(B FileException) on Posix when could not move the item to its original location or could not recreate original location directory.$(BR)
748          *  $(B Exception) on other errors.
749          */
750         @safe void restore(ref scope TrashcanItem item) {}
751         /**
752          * Erase item from trashcan.
753          * Throws:
754          *  $(B WindowsException) on Windows when the operation failed.$(BR)
755          *  $(B FileException) on Posix when could not delete the item.$(BR)
756          *  $(B Exception) on other errors.
757          */
758         @safe void erase(ref scope TrashcanItem item) {}
759         /**
760          * The name of trashcan (possibly localized). Currently implemented only for Windows and KDE, and returns empty string on other platforms.
761          * Returns:
762          *  Name of trashcan as defined by system for the current user. Empty string if the name is unknown.
763          */
764         @property @safe string displayName() nothrow {return string.init;}
765 
766         static if (!is(typeof(IShellFolder2.init)))
767         {
768             static struct IShellFolder2 {}
769         }
770         /**
771          * Windows-only function to get $(LINK2 https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/nn-shobjidl_core-ishellfolder2, IShellFolder2) object associated with recycle bin.
772          *
773          * Note:
774          *  If you want a returned object to outlive $(D Trashcan), you must call AddRef on it (and then Release when it's no longer needed).
775          */
776         @system @property @nogc IShellFolder2 recycleBin() nothrow {return IShellFolder2.init;}
777     }
778 }
779 else version(Windows) final class Trashcan : ITrashcan
780 {
781     @trusted this() {
782         CoInitializeEx(null, COINIT.COINIT_APARTMENTTHREADED);
783         IShellFolder desktop;
784         LPITEMIDLIST pidlRecycleBin;
785 
786         henforce(SHGetDesktopFolder(&desktop), "Failed to get desktop shell folder");
787         assert(desktop);
788         scope(exit) desktop.Release();
789         henforce(SHGetSpecialFolderLocation(null, CSIDL_BITBUCKET, &pidlRecycleBin), "Failed to get recycle bin location");
790         assert(pidlRecycleBin);
791         scope(exit) ILFree(pidlRecycleBin);
792 
793         henforce(desktop.BindToObject(pidlRecycleBin, null, &IID_IShellFolder2, cast(LPVOID *)&_recycleBin), "Failed to get recycle bin shell folder");
794         assert(_recycleBin);
795         collectException(getDisplayNameOf(desktop, pidlRecycleBin), _displayName);
796     }
797 
798     @trusted ~this() {
799         assert(_recycleBin);
800         _recycleBin.Release();
801         CoUninitialize();
802     }
803 
804     private static struct ByItem
805     {
806         this(IShellFolder2 folder) {
807             this.folder = folder;
808             folder.AddRef();
809             with(SHCONTF) henforce(folder.EnumObjects(null, SHCONTF_FOLDERS | SHCONTF_NONFOLDERS | SHCONTF_INCLUDEHIDDEN, &enumFiles), "Failed to enumerate objects in recycle bin");
810             popFront();
811         }
812         this(this) {
813             if (enumFiles)
814                 enumFiles.AddRef();
815             if (folder)
816                 folder.AddRef();
817         }
818         ~this() {
819             if (enumFiles)
820                 enumFiles.Release();
821             if (folder)
822                 folder.Release();
823         }
824         TrashcanItem front() {
825             return current;
826         }
827         TrashcanItem moveFront() {
828             import std.algorithm.mutation : move;
829             return move(current);
830         }
831         void popFront() {
832             LPITEMIDLIST pidl;
833             if (enumFiles.Next(1, &pidl, null) == S_FALSE) {
834                 atTheEnd = true;
835             } else {
836                 assert(pidl);
837                 ULONG attributes = SFGAOF.SFGAO_FOLDER;
838                 folder.GetAttributesOf(1,cast(LPCITEMIDLIST *)&pidl,&attributes);
839                 string fileName = getDisplayNameOf(folder, pidl);
840                 string extension = getStringDetailOf(folder, pidl, 166);
841                 SysTime deletionTime = getSysTimeDetailOf(folder, pidl, 2);
842                 // The returned name may or may not contain the extension depending on the view parameters of the recycle bin folder
843                 if (fileName.extension != extension)
844                     fileName ~= extension;
845                 current = TrashcanItem(fileName, !!(attributes & SFGAOF.SFGAO_FOLDER), deletionTime, pidl);
846             }
847         }
848         bool empty() {
849             return atTheEnd;
850         }
851     private:
852         IShellFolder2 folder;
853         IEnumIDList enumFiles;
854         TrashcanItem current;
855         bool atTheEnd;
856     }
857 
858     @trusted InputRange!TrashcanItem byItem() {
859         return inputRangeObject(ByItem(_recycleBin));
860     }
861 
862     private @trusted void trustedRestore(ref scope TrashcanItem item) {
863         //RunVerb!"undelete"(_recycleBin, item.itemIdList);
864         RunRestoreOperation(_recycleBin, item.itemIdList);
865     }
866     @safe void restore(ref scope TrashcanItem item) {
867         trustedRestore(item);
868     }
869     private @trusted void trustedErase(ref scope TrashcanItem item) {
870         //RunVerb!"delete"(_recycleBin, item.itemIdList);
871         RunDeleteOperation(_recycleBin, item.itemIdList);
872     }
873     @safe void erase(ref scope TrashcanItem item) {
874         trustedErase(item);
875     }
876     @property @safe string displayName() nothrow {
877         return _displayName;
878     }
879     @property @system @nogc IShellFolder2 recycleBin() nothrow {
880         return _recycleBin;
881     }
882 private:
883     string _displayName;
884     IShellFolder2 _recycleBin;
885 } else static if (isFreedesktop)
886 {
887     final class Trashcan : ITrashcan
888     {
889         private @safe static bool isDirNothrow(string path) nothrow {
890             bool isDirectory;
891             if (collectException(path.isDir, isDirectory) is null)
892                 return isDirectory;
893             return false;
894         }
895 
896         @safe this() {}
897 
898         import std.typecons : Tuple;
899 
900         @trusted InputRange!TrashcanItem byItem() {
901             import std.algorithm.iteration : cache, map, joiner, filter;
902             alias Tuple!(string, "base", string, "info", string, "files", string, "root") TrashLocation;
903             return inputRangeObject(standardTrashBasePaths().map!(trashDir => TrashLocation(trashDir.base, buildPath(trashDir.base, "info"), buildPath(trashDir.base, "files"), trashDir.root)).filter!(t => isDirNothrow(t.info) && isDirNothrow(t.files)).map!(delegate(TrashLocation trash) {
904                 InputRange!TrashcanItem toReturn;
905                 try {
906                     toReturn = inputRangeObject(dirEntries(trash.info, SpanMode.shallow, false).filter!(entry => entry.extension == ".trashinfo").map!(delegate(DirEntry entry) {
907                         string trashedFile = buildPath(trash.files, entry.baseName.stripExtension);
908                         try {
909                             if (exists(trashedFile)) {
910                                 import inilike.read;
911 
912                                 string path;
913                                 SysTime deletionTime;
914 
915                                 auto onGroup = delegate ActionOnGroup(string groupName) {
916                                     if (groupName == "Trash Info")
917                                         return ActionOnGroup.stopAfter;
918                                     return ActionOnGroup.skip;
919                                 };
920                                 auto onKeyValue = delegate void(string key, string value, string groupName) {
921                                     if (groupName == "Trash Info")
922                                     {
923                                         if (key == "Path")
924                                             path = value;
925                                         else if (key == "DeletionDate")
926                                             collectException(SysTime.fromISOExtString(value), deletionTime);
927                                     }
928                                 };
929                                 readIniLike(iniLikeFileReader(entry.name), null, onGroup, onKeyValue, null);
930 
931                                 if (path.length) {
932                                     path = path.decode();
933                                     string restorePath;
934                                     if (path.isAbsolute)
935                                         restorePath = path;
936                                     else
937                                         restorePath = buildPath(trash.root, path);
938                                     return TrashcanItem(restorePath, trashedFile.isDir, deletionTime, entry.name, trashedFile);
939                                 }
940                             }
941                         } catch(Exception e) {}
942                         return TrashcanItem.init;
943                     }).cache.filter!(item => item.restorePath.length));
944                 } catch(Exception e) {
945                     toReturn = inputRangeObject(TrashcanItem[].init);
946                 }
947                 return toReturn;
948             }).cache.joiner);
949         }
950 
951         @safe void restore(ref scope TrashcanItem item) {
952             mkdirRecurse(item.restorePath.dirName);
953             rename(item.trashedPath, item.restorePath);
954             collectException(remove(item.trashInfoPath));
955         }
956         @safe void erase(ref scope TrashcanItem item) {
957             static @trusted void trustedErase(string path)
958             {
959                 if (path.isDir)
960                     rmdirRecurse(path);
961                 else
962                     remove(path);
963             }
964             trustedErase(item.trashedPath);
965             collectException(remove(item.trashInfoPath));
966         }
967         @property @safe string displayName() nothrow {
968             if (!_triedToRetrieveName) {
969                 _triedToRetrieveName = true;
970 
971                 static @safe string currentLocale() nothrow
972                 {
973                     import std.process : environment;
974                     try {
975                         return environment.get("LC_ALL", environment.get("LC_MESSAGES", environment.get("LANG")));
976                     } catch(Exception e) {
977                         return null;
978                     }
979                 }
980 
981                 static @trusted string readTrashName(scope const(string)[] desktopFiles, scope string locale) nothrow {
982                     foreach(path; desktopFiles) {
983                         if (!path.exists)
984                             continue;
985                         try {
986                             import inilike.read;
987                             import inilike.common;
988 
989                             string name;
990                             string bestLocale;
991 
992                             auto onGroup = delegate ActionOnGroup(string groupName) {
993                                 if (groupName == "Desktop Entry")
994                                     return ActionOnGroup.stopAfter;
995                                 return ActionOnGroup.skip;
996                             };
997                             auto onKeyValue = delegate void(string key, string value, string groupName) {
998                                 if (groupName == "Desktop Entry")
999                                 {
1000                                     auto keyAndLocale = separateFromLocale(key);
1001                                     if (keyAndLocale[0] == "Name")
1002                                     {
1003                                         auto lv = selectLocalizedValue(locale, keyAndLocale[1], value, bestLocale, name);
1004                                         bestLocale = lv[0];
1005                                         name = lv[1].unescapeValue();
1006                                     }
1007                                 }
1008                             };
1009                             readIniLike(iniLikeFileReader(path), null, onGroup, onKeyValue, null);
1010                             if (name.length)
1011                                 return name;
1012                         } catch(Exception e) {}
1013                     }
1014                     return string.init;
1015                 }
1016 
1017                 const locale = currentLocale();
1018                 _displayName = readTrashName(xdgAllDataDirs("kio_desktop/directory.trash"), locale);
1019                 if (!_displayName.length) {
1020                     _displayName = readTrashName(xdgAllDataDirs("kde4/apps/kio_desktop/directory.trash"), locale);
1021                 }
1022             }
1023             /+
1024             On GNOME it can be read from nautilus translation file (.mo).
1025             +/
1026             return _displayName;
1027         }
1028     private:
1029         alias Tuple!(string, "base", string, "root") TrashRoot;
1030         @trusted static TrashRoot[] standardTrashBasePaths() {
1031             TrashRoot[] trashBasePaths;
1032             import core.sys.posix.unistd;
1033 
1034             string homeTrashPath = xdgDataHome("Trash");
1035             string homeTrashTopDir;
1036             if (homeTrashPath.length && homeTrashPath.isAbsolute && isDirNothrow(homeTrashPath)) {
1037                 homeTrashTopDir = homeTrashPath.topDir;
1038                 trashBasePaths ~= TrashRoot(homeTrashPath, homeTrashTopDir);
1039             }
1040 
1041             auto userId = getuid();
1042             auto volumes = mountedVolumes();
1043             foreach(volume; volumes) {
1044                 if (!volume.isValid)
1045                     continue;
1046                 if (homeTrashTopDir == volume.path)
1047                     continue;
1048                 string diskTrash;
1049                 string userTrash;
1050                 if (collectException(checkDiskTrash(volume.path), diskTrash) is null) {
1051                     userTrash = userTrashSubdir(diskTrash, userId);
1052                     if (isDirNothrow(userTrash))
1053                         trashBasePaths ~= TrashRoot(userTrash, volume.path);
1054                 }
1055                 userTrash = userTrashDir(volume.path, userId);
1056                 if (isDirNothrow(userTrash))
1057                     trashBasePaths ~= TrashRoot(userTrash, volume.path);
1058             }
1059             return trashBasePaths;
1060         }
1061         string _displayName;
1062         bool _triedToRetrieveName;
1063     }
1064 }