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 }