1 /**
2  * Parse Shell Link files (.lnk).
3  * Authors: 
4  *  $(LINK2 https://github.com/MyLittleRobo, Roman Chistokhodov)
5  * Copyright:
6  *  Roman Chistokhodov, 2016
7  * License: 
8  *  $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
9  * See_Also: 
10  *  $(LINK2 https://msdn.microsoft.com/en-us/library/dd871305.aspx, Shell Link Binary File Format)
11  */
12 
13 module lnk;
14 
15 private {
16     import std.traits;
17     import std.bitmanip;
18     import std.file;
19     import std.system;
20     import std.exception;
21     import std.utf;
22 }
23 
24 private @nogc @trusted void swapByteOrder(T)(ref T t) nothrow pure  {
25     
26     static if( __VERSION__ < 2067 ) { //swapEndian was not @nogc
27         ubyte[] bytes = (cast(ubyte*)&t)[0..T.sizeof];
28         for (size_t i=0; i<bytes.length/2; ++i) {
29             ubyte tmp = bytes[i];
30             bytes[i] = bytes[T.sizeof-1-i];
31             bytes[T.sizeof-1-i] = tmp;
32         }
33     } else {
34         t = swapEndian(t);
35     }
36 }
37 
38 private @trusted T readValue(T)(const(ubyte)[] data) if (isIntegral!T || isSomeChar!T)
39 {
40     if (data.length >= T.sizeof) {
41         T value = *(cast(const(T)*)data[0..T.sizeof].ptr);
42         static if (endian == Endian.bigEndian) {
43             swapByteOrder(value);
44         }
45         return value;
46     } else {
47         throw new ShellLinkException("Value of requrested size is out of data bounds");
48     }
49 }
50 
51 private @trusted T eatValue(T)(ref const(ubyte)[] data) if (isIntegral!T || isSomeChar!T)
52 {
53     auto value = readValue!T(data);
54     data = data[T.sizeof..$];
55     return value;
56 }
57 
58 private @trusted const(T)[] readSlice(T = ubyte)(const(ubyte)[] data, size_t count) if (isIntegral!T || isSomeChar!T)
59 {
60     if (data.length >= count*T.sizeof) {
61         return cast(typeof(return))data[0..count*T.sizeof];
62     } else {
63         throw new ShellLinkException("Slice of requsted size is out of data bounds");
64     }
65 }
66 
67 private @trusted const(T)[] eatSlice(T = ubyte)(ref const(ubyte)[] data, size_t count) if (isIntegral!T || isSomeChar!T)
68 {
69     auto slice = readSlice!T(data, count);
70     data = data[count*T.sizeof..$];
71     return slice;
72 }
73 
74 private @trusted const(char)[] readString(const(ubyte)[] data)
75 {
76     auto str = cast(const(char)[])data;
77     for (size_t i=0; i<str.length; ++i) {
78         if (data[i] == 0) {
79             return str[0..i];
80         }
81     }
82     throw new ShellLinkException("Could not read null-terminated string");
83 }
84 
85 private @trusted const(wchar)[] readWString(const(ubyte)[] data)
86 {
87     auto possibleWcharCount = data.length/2; //to chop the last byte if count is odd, avoid misalignment
88     auto str = cast(const(wchar)[])data[0..possibleWcharCount*2];
89     for (size_t i=0; i<str.length; ++i) {
90         if (str[i] == 0) {
91             return str[0..i];
92         }
93     }
94     throw new ShellLinkException("Could not read null-terminated wide string");
95 }
96 
97 
98 /**
99  * Exception thrown if shell link file data could not be parsed.
100  */
101 final class ShellLinkException : Exception
102 {
103     this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null) pure nothrow @safe {
104         super(msg, file, line, next);
105     }
106 }
107 
108 version(Windows) {
109     import core.sys.windows.windows : CommandLineToArgvW, LocalFree;
110     import core.stdc.wchar_;
111     
112     private @trusted string[] parseCommandLine(string commandLine)
113     {
114         auto wCommandLineZ = ("Dummy.exe " ~ commandLine).toUTF16z();
115         int argc;
116         auto argv = CommandLineToArgvW(wCommandLineZ, &argc);
117         if (argv is null || argc == 0) {
118             return null;
119         }
120         scope(exit) LocalFree(argv);
121         
122         string[] args;
123         args.length = argc-1;
124         for (size_t i=0; i<args.length; ++i) {
125             args[i] = argv[i+1][0..wcslen(argv[i+1])].toUTF8();
126         }
127         return args;
128     }
129 }
130 
131 private @trusted string fromANSIToUnicode(const(char)[] ansi)
132 {
133     version(Windows) {
134         import core.sys.windows.windows : MultiByteToWideChar;
135         auto requiredLength = MultiByteToWideChar(0, 0, ansi.ptr, ansi.length, null, 0);
136         if (requiredLength) {
137             auto wstr = new wchar[requiredLength];
138             auto bytesWritten = MultiByteToWideChar(0, 0, ansi.ptr, ansi.length, wstr.ptr, wstr.length);
139             if (bytesWritten) {
140                 if (wstr[$-1] == 0) {
141                     wstr = wstr[0..$-1];
142                 }
143                 return wstr.toUTF8();
144             }
145         }
146         return null;
147     } else {
148         ///TODO: implement for non-Windows.
149         return ansi.idup;
150     }
151 }
152 
153 /**
154  * Class for accessing Shell Link objects (.lnk files)
155  */
156 final class ShellLink 
157 {
158 private:
159     Header _header;
160     ubyte[][] _itemIdList;
161     LinkInfoHeader _linkInfoHeader;
162     Volume _volume;
163     CommonNetworkRelativeLink _networkLink;
164     
165     string _localBasePath;
166     string _commonPathSuffix;
167     
168     string _name;
169     string _relativePath;
170     string _workingDir;
171     string _arguments;
172     string _iconLocation;
173     
174     string _netName;
175     string _deviceName;
176     
177     string _fileName;
178     
179 public:
180     /**
181      * Read Shell Link from fileName.
182      * Throws: 
183      *  FileException is file could not be read.
184      *  ShellLinkException if file could not be parsed.
185      * Note: file will be read as whole.
186      */
187     @trusted this(string fileName)
188     {
189         this(cast(const(ubyte)[])read(fileName), fileName);
190     }
191 
192     /**
193      * Read Shell Link from data. fileName should be path to the .lnk file where data was read from.
194      * Throws:
195      *  ShellLinkException if data could not be parsed.
196      */
197     @safe this(const(ubyte)[] data, string fileName = null)
198     {
199         _fileName = fileName;
200         auto headerSize = readValue!uint(data);
201         enforce!ShellLinkException(headerSize == Header.requiredHeaderSize, "Wrong Shell Link Header size");
202         auto headerData = eatSlice(data, headerSize);
203         _header = parseHeader(headerData);
204         
205         if (_header.linkFlags & HasLinkTargetIDList) {
206             auto idListSize = eatValue!ushort(data);
207             auto idListData = eatSlice(data, idListSize);
208             _itemIdList = parseItemIdList(idListData);
209         }
210         
211         if (_header.linkFlags & HasLinkInfo) {
212             auto linkInfoSize = readValue!uint(data);
213             auto linkInfoData = eatSlice(data, linkInfoSize);
214             _linkInfoHeader = parseLinkInfo(linkInfoData);
215             
216             if (_linkInfoHeader.localBasePathOffsetUnicode) {
217                 _localBasePath = readWString(linkInfoData[_linkInfoHeader.localBasePathOffsetUnicode..$]).toUTF8();
218             } else if (_linkInfoHeader.localBasePathOffset) {
219                 auto str = readString(linkInfoData[_linkInfoHeader.localBasePathOffset..$]);
220                 _localBasePath = fromANSIToUnicode(str);
221             }
222             if (_linkInfoHeader.commonPathSuffixOffsetUnicode) {
223                 _commonPathSuffix = readWString(linkInfoData[_linkInfoHeader.commonPathSuffixOffsetUnicode..$]).toUTF8();
224             } else if (_linkInfoHeader.commonPathSuffixOffset) {
225                 auto str = readString(linkInfoData[_linkInfoHeader.commonPathSuffixOffset..$]);
226                 _commonPathSuffix = fromANSIToUnicode(str);
227             }
228             
229             if (_linkInfoHeader.flags & VolumeIDAndLocalBasePath && _linkInfoHeader.volumeIdOffset) {
230                 auto volumeIdSize = readValue!uint(linkInfoData[_linkInfoHeader.volumeIdOffset..$]);
231                 enforce!ShellLinkException(volumeIdSize > Volume.minimumSize, "Wrong VolumeID size");
232                 auto volumeIdData = readSlice(linkInfoData[_linkInfoHeader.volumeIdOffset..$], volumeIdSize);
233                 _volume = parseVolumeData(volumeIdData);
234             }
235             
236             if (_linkInfoHeader.flags & CommonNetworkRelativeLinkAndPathSuffix && _linkInfoHeader.commonNetworkRelativeLinkOffset) {
237                 auto networkLinkSize = readValue!uint(linkInfoData[_linkInfoHeader.commonNetworkRelativeLinkOffset..$]);
238                 enforce!ShellLinkException(networkLinkSize >= CommonNetworkRelativeLink.minimumSize, "Wrong common network relative path link size");
239                 auto networkLinkData = readSlice(linkInfoData[_linkInfoHeader.commonNetworkRelativeLinkOffset..$], networkLinkSize);
240                 _networkLink = parseNetworkLink(networkLinkData);
241                 
242                 if (_networkLink.netNameOffsetUnicode) {
243                     _netName = readWString(networkLinkData[_networkLink.netNameOffsetUnicode..$]).toUTF8();
244                 } else if (_networkLink.netNameOffset) {
245                     auto str = readString(networkLinkData[_networkLink.netNameOffset..$]);
246                     _netName = fromANSIToUnicode(str);
247                 }
248                 
249                 if (_networkLink.deviceNameOffsetUnicode) {
250                     _deviceName = readWString(networkLinkData[_networkLink.deviceNameOffsetUnicode..$]).toUTF8();
251                 } else if (_networkLink.deviceNameOffset) {
252                     auto str = readString(networkLinkData[_networkLink.deviceNameOffset..$]);
253                     _deviceName = fromANSIToUnicode(str);
254                 }
255             }
256         }
257         
258         if (_header.linkFlags & HasName) {
259             _name = consumeStringData(data);
260         }
261         if (_header.linkFlags & HasRelativePath) {
262             _relativePath = consumeStringData(data);
263         }
264         if (_header.linkFlags & HasWorkingDir) {
265             _workingDir = consumeStringData(data);
266         }
267         if (_header.linkFlags & HasArguments) {
268             _arguments = consumeStringData(data);
269         }
270         if (_header.linkFlags & HasIconLocation) {
271             _iconLocation = consumeStringData(data);
272         }
273     }
274     
275     /**
276      * Get description of for a Shell Link object.
277      */
278     @nogc @safe string description() const nothrow {
279         return _name;
280     }
281     
282     /**
283      * Get relative path of for a Shell Link object.
284      */
285     @nogc @safe string relativePath() const nothrow {
286         return _relativePath;
287     }
288     
289     /**
290      * Get working directory of for a Shell Link object.
291      */
292     @nogc @safe string workingDirectory() const nothrow {
293         return _workingDir;
294     }
295     
296     /**
297      * Get arguments of for a Shell Link object as one string. Target file path is NOT included.
298      */
299     @nogc @safe string argumentsString() const nothrow {
300         return _arguments;
301     }
302     
303     version(Windows) {
304         /**
305          * Get command line arguments. Target file path is NOT included.
306          * Note: this function is Windows only.
307          */
308         @safe string[] arguments() const {
309             return parseCommandLine(_arguments);
310         }
311     }
312     
313     /**
314      * Get icon location of for a Shell Link object.
315      */
316     @nogc @safe string iconLocation() const nothrow {
317         return _iconLocation;
318     }
319     
320     /**
321      * Resolve target file location.
322      * Note: In case path parts were stored only as ANSI 
323      * the result string may contain garbage characters if function is ran on other system than Windows 
324      * or if user changes default code page and shell link did not get updated.
325      * If path parts were stored as Unicode it should not have problems.
326      */
327     @safe string resolve() const {
328         if (_netName.length) {
329             return _netName ~ '\\' ~ _commonPathSuffix;
330         } else {
331             return _localBasePath ~ _commonPathSuffix;
332         }
333     }
334     
335     /**
336      * Get path of link object as was specified upon constructing.
337      */
338     @nogc @safe string fileName() const nothrow {
339         return _fileName;
340     }
341     
342 private:
343     @trusted static string consumeStringData(ref const(ubyte)[] data)
344     {
345         auto size = eatValue!ushort(data);
346         return eatSlice!wchar(data, size).toUTF8();
347     }
348 
349     enum : uint {
350         HasLinkTargetIDList = 1 << 0,
351         HasLinkInfo = 1 << 1,
352         HasName = 1 << 2,
353         HasRelativePath = 1 << 3,
354         HasWorkingDir = 1 << 4,
355         HasArguments = 1 << 5,
356         HasIconLocation = 1 << 6,
357         IsUnicode = 1 << 7,
358         ForceNoLinkInfo = 1 << 8,
359         HasExpString = 1 << 9,
360         RunInSeparateProcess = 1 << 10,
361         Unused1 = 1 << 11,
362         HasDarwinID = 1 << 12,
363         RunAsUser = 1 << 13,
364         HasExpIcon = 1 << 14,
365         NoPidlAlias = 1 << 15,
366         Unused2 = 1 << 16,
367         RunWithShimLayer = 1 << 17,
368         ForceNoLinkTrack = 1 << 18,
369         EnableTargetMetadata = 1 << 19,
370         DisableLinkPathTracking = 1 << 20,
371         DisableKnownFolderTracking = 1 << 21,
372         DisableKnownFolderAlias = 1 << 22,
373         AllowLinkToLink = 1 << 23,
374         UnaliasOnSave = 1 << 24,
375         PreferEnvironmentPath = 1 << 25,
376         KeepLocalIDListForUNCTarget = 1 << 26
377     }
378 
379     struct Header
380     {
381         alias ubyte[16] CLSID;
382 
383         enum uint requiredHeaderSize = 0x0000004C;
384         enum CLSID requiredLinkCLSID = [1, 20, 2, 0, 0, 0, 0, 0, 192, 0, 0, 0, 0, 0, 0, 70];
385         uint headerSize;
386         CLSID linkCLSID;
387         uint linkFlags;
388         uint fileAttributes;
389         ulong creationTime;
390         ulong accessTime;
391         ulong writeTime;
392         uint fileSize;
393         uint iconIndex;
394         uint showCommand;
395         
396         enum {
397             SW_SHOWNORMAL = 0x00000001,
398             SW_SHOWMAXIMIZED = 0x00000003,
399             SW_SHOWMINNOACTIVE = 0x00000007
400         }
401         
402         ushort hotKey;
403         ushort reserved1;
404         uint reserved2;
405         uint reserved3;
406     }
407 
408     @trusted static Header parseHeader(const(ubyte)[] headerData)
409     {
410         Header header;
411         header.headerSize = eatValue!uint(headerData);
412         auto linkCLSIDSlice = eatSlice(headerData, 16);
413         
414         enforce!ShellLinkException(linkCLSIDSlice == Header.requiredLinkCLSID[], "Invalid Link CLSID");
415         for (size_t i=0; i<16; ++i) {
416             header.linkCLSID[i] = linkCLSIDSlice[i];
417         }
418         
419         header.linkFlags = eatValue!uint(headerData);
420         
421         header.fileAttributes = eatValue!uint(headerData);
422         header.creationTime = eatValue!ulong(headerData);
423         header.accessTime = eatValue!ulong(headerData);
424         header.writeTime = eatValue!ulong(headerData);
425         header.fileSize = eatValue!uint(headerData);
426         header.iconIndex = eatValue!uint(headerData);
427         header.showCommand = eatValue!uint(headerData);
428         header.hotKey = eatValue!ushort(headerData);
429         
430         header.reserved1 = eatValue!ushort(headerData);
431         header.reserved2 = eatValue!uint(headerData);
432         header.reserved3 = eatValue!uint(headerData);
433         return header;
434     }
435     
436     @trusted static ubyte[][] parseItemIdList(const(ubyte)[] idListData) 
437     {
438         ubyte[][] itemIdList;
439         while(true) {
440             auto itemSize = eatValue!ushort(idListData);
441             if (itemSize) {
442                 enforce(itemSize >= 2, "Item size must be at least 2");
443                 auto dataSize = itemSize - 2;
444                 auto itemData = eatSlice(idListData, dataSize);
445                 itemIdList ~= itemData.dup;
446             } else {
447                 break;
448             }
449         }
450         return itemIdList;
451     }
452     
453     enum {
454         VolumeIDAndLocalBasePath = 1 << 0,
455         CommonNetworkRelativeLinkAndPathSuffix = 1 << 1
456     }
457     
458     struct LinkInfoHeader
459     {
460         enum uint defaultHeaderSize = 0x1C;
461         enum uint minimumExtendedHeaderSize = 0x24;
462     
463         uint infoSize;
464         uint headerSize;
465         uint flags;
466         uint volumeIdOffset;
467         uint localBasePathOffset;
468         uint commonNetworkRelativeLinkOffset;
469         uint commonPathSuffixOffset;
470         uint localBasePathOffsetUnicode;
471         uint commonPathSuffixOffsetUnicode;
472     }
473     
474     @trusted static LinkInfoHeader parseLinkInfo(const(ubyte[]) linkInfoData)
475     {
476         LinkInfoHeader linkInfoHeader;
477         linkInfoHeader.infoSize = readValue!uint(linkInfoData);
478         linkInfoHeader.headerSize = readValue!uint(linkInfoData[uint.sizeof..$]);
479         
480         auto linkInfoHeaderData = readSlice(linkInfoData, linkInfoHeader.headerSize);
481         eatSlice(linkInfoHeaderData, uint.sizeof*2);
482         
483         linkInfoHeader.flags = eatValue!uint(linkInfoHeaderData);
484         linkInfoHeader.volumeIdOffset = eatValue!uint(linkInfoHeaderData);
485         linkInfoHeader.localBasePathOffset = eatValue!uint(linkInfoHeaderData);
486         linkInfoHeader.commonNetworkRelativeLinkOffset = eatValue!uint(linkInfoHeaderData);
487         linkInfoHeader.commonPathSuffixOffset = eatValue!uint(linkInfoHeaderData);
488         
489         if (linkInfoHeader.headerSize == LinkInfoHeader.defaultHeaderSize) {
490             //ok, no additional fields
491         } else if (linkInfoHeader.headerSize >= LinkInfoHeader.minimumExtendedHeaderSize) {
492             linkInfoHeader.localBasePathOffsetUnicode = eatValue!uint(linkInfoHeaderData);
493             linkInfoHeader.commonPathSuffixOffsetUnicode = eatValue!uint(linkInfoHeaderData);
494         } else {
495             throw new ShellLinkException("Bad LinkInfoHeaderSize");
496         }
497         return linkInfoHeader;
498     }
499     
500     struct Volume
501     {
502         enum uint minimumSize = 0x10;
503         uint size;
504         uint driveType;
505         uint driveSerialNumber;
506         uint labelOffset;
507         uint labelOffsetUnicode;
508         ubyte[] data;
509     }
510     
511     @trusted static Volume parseVolumeData(const(ubyte)[] volumeIdData)
512     {
513         Volume volume;
514         volume.size = eatValue!uint(volumeIdData);
515         volume.driveType = eatValue!uint(volumeIdData);
516         volume.driveSerialNumber = eatValue!uint(volumeIdData);
517         volume.labelOffset = eatValue!uint(volumeIdData);
518         if (volume.labelOffset == 0x14) {
519             volume.labelOffsetUnicode = eatValue!uint(volumeIdData);
520         }
521         volume.data = volumeIdData.dup;
522         return volume;
523     }
524     
525     struct CommonNetworkRelativeLink
526     {
527         enum uint minimumSize = 0x14;
528         uint size;
529         uint flags;
530         uint netNameOffset;
531         uint deviceNameOffset;
532         uint networkProviderType;
533         uint netNameOffsetUnicode;
534         uint deviceNameOffsetUnicode;
535     }
536     
537     @trusted static CommonNetworkRelativeLink parseNetworkLink(const(ubyte)[] networkLinkData)
538     {
539         CommonNetworkRelativeLink networkLink;
540         networkLink.size = eatValue!uint(networkLinkData);
541         networkLink.flags = eatValue!uint(networkLinkData);
542         networkLink.netNameOffset = eatValue!uint(networkLinkData);
543         networkLink.deviceNameOffset = eatValue!uint(networkLinkData);
544         networkLink.networkProviderType = eatValue!uint(networkLinkData);
545         
546         if (networkLink.netNameOffset > CommonNetworkRelativeLink.minimumSize) {
547             networkLink.netNameOffsetUnicode = eatValue!uint(networkLinkData);
548             networkLink.deviceNameOffsetUnicode = eatValue!uint(networkLinkData);
549         }
550         
551         return networkLink;
552     }
553 }