001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.tools; 003 004import java.io.ByteArrayOutputStream; 005import java.io.File; 006import java.io.IOException; 007import java.io.InputStream; 008import java.nio.charset.StandardCharsets; 009import java.nio.file.Files; 010import java.text.ParseException; 011import java.util.Locale; 012 013/** 014 * Represents a Windows shortcut (typically visible to Java only as a '.lnk' file). 015 * 016 * Retrieved 2011-09-23 from http://stackoverflow.com/questions/309495/windows-shortcut-lnk-parser-in-java/672775#672775 017 * 018 * Written by: (the stack overflow users, obviously!) 019 * Apache Commons VFS dependency removed by crysxd (why were we using that!?) https://github.com/crysxd 020 * Headerified, refactored and commented by Code Bling http://stackoverflow.com/users/675721/code-bling 021 * Network file support added by Stefan Cordes http://stackoverflow.com/users/81330/stefan-cordes 022 * Adapted by Sam Brightman http://stackoverflow.com/users/2492/sam-brightman 023 * Based on information in 'The Windows Shortcut File Format' by Jesse Hager <jessehager@iname.com> 024 * And somewhat based on code from the book 'Swing Hacks: Tips and Tools for Killer GUIs' 025 * by Joshua Marinacci and Chris Adamson 026 * ISBN: 0-596-00907-0 027 * http://www.oreilly.com/catalog/swinghks/ 028 * @since 13692 029 */ 030public class WindowsShortcut { 031 private boolean isDirectory; 032 private boolean isLocal; 033 private String realFile; 034 035 /** 036 * Provides a quick test to see if this could be a valid link ! 037 * If you try to instantiate a new WindowShortcut and the link is not valid, 038 * Exceptions may be thrown and Exceptions are extremely slow to generate, 039 * therefore any code needing to loop through several files should first check this. 040 * 041 * @param file the potential link 042 * @return true if may be a link, false otherwise 043 * @throws IOException if an IOException is thrown while reading from the file 044 */ 045 public static boolean isPotentialValidLink(File file) throws IOException { 046 final int minimumLength = 0x64; 047 boolean isPotentiallyValid = false; 048 try (InputStream fis = Files.newInputStream(file.toPath())) { 049 isPotentiallyValid = file.isFile() 050 && file.getName().toLowerCase(Locale.ENGLISH).endsWith(".lnk") 051 && fis.available() >= minimumLength 052 && isMagicPresent(getBytes(fis, 32)); 053 } 054 return isPotentiallyValid; 055 } 056 057 /** 058 * Constructs a new {@code WindowsShortcut} 059 * @param file file 060 * @throws IOException if an I/O error occurs 061 * @throws ParseException if a parsing error occurs 062 */ 063 public WindowsShortcut(File file) throws IOException, ParseException { 064 try (InputStream in = Files.newInputStream(file.toPath())) { 065 parseLink(getBytes(in)); 066 } 067 } 068 069 /** 070 * @return the name of the filesystem object pointed to by this shortcut 071 */ 072 public String getRealFilename() { 073 return realFile; 074 } 075 076 /** 077 * Tests if the shortcut points to a local resource. 078 * @return true if the 'local' bit is set in this shortcut, false otherwise 079 */ 080 public boolean isLocal() { 081 return isLocal; 082 } 083 084 /** 085 * Tests if the shortcut points to a directory. 086 * @return true if the 'directory' bit is set in this shortcut, false otherwise 087 */ 088 public boolean isDirectory() { 089 return isDirectory; 090 } 091 092 /** 093 * Gets all the bytes from an InputStream 094 * @param in the InputStream from which to read bytes 095 * @return array of all the bytes contained in 'in' 096 * @throws IOException if an IOException is encountered while reading the data from the InputStream 097 */ 098 private static byte[] getBytes(InputStream in) throws IOException { 099 return getBytes(in, null); 100 } 101 102 /** 103 * Gets up to max bytes from an InputStream 104 * @param in the InputStream from which to read bytes 105 * @param max maximum number of bytes to read 106 * @return array of all the bytes contained in 'in' 107 * @throws IOException if an IOException is encountered while reading the data from the InputStream 108 */ 109 private static byte[] getBytes(InputStream in, Integer max) throws IOException { 110 // read the entire file into a byte buffer 111 ByteArrayOutputStream bout = new ByteArrayOutputStream(); 112 byte[] buff = new byte[256]; 113 while (max == null || max > 0) { 114 int n = in.read(buff); 115 if (n == -1) { 116 break; 117 } 118 bout.write(buff, 0, n); 119 if (max != null) 120 max -= n; 121 } 122 in.close(); 123 return bout.toByteArray(); 124 } 125 126 private static boolean isMagicPresent(byte[] link) { 127 final int magic = 0x0000004C; 128 final int magicOffset = 0x00; 129 return link.length >= 32 && bytesToDword(link, magicOffset) == magic; 130 } 131 132 /** 133 * Gobbles up link data by parsing it and storing info in member fields 134 * @param link all the bytes from the .lnk file 135 * @throws ParseException if a parsing error occurs 136 */ 137 private void parseLink(byte[] link) throws ParseException { 138 try { 139 if (!isMagicPresent(link)) 140 throw new ParseException("Invalid shortcut; magic is missing", 0); 141 142 // get the flags byte 143 byte flags = link[0x14]; 144 145 // get the file attributes byte 146 final int fileAttsOffset = 0x18; 147 byte fileAtts = link[fileAttsOffset]; 148 byte isDirMask = (byte) 0x10; 149 if ((fileAtts & isDirMask) != 0) { 150 isDirectory = true; 151 } else { 152 isDirectory = false; 153 } 154 155 // if the shell settings are present, skip them 156 final int shellOffset = 0x4c; 157 final byte hasShellMask = (byte) 0x01; 158 int shellLen = 0; 159 if ((flags & hasShellMask) != 0) { 160 // the plus 2 accounts for the length marker itself 161 shellLen = bytesToWord(link, shellOffset) + 2; 162 } 163 164 // get to the file settings 165 int fileStart = 0x4c + shellLen; 166 167 final int fileLocationInfoFlagOffsetOffset = 0x08; 168 int fileLocationInfoFlag = link[fileStart + fileLocationInfoFlagOffsetOffset]; 169 isLocal = (fileLocationInfoFlag & 2) == 0; 170 // get the local volume and local system values 171 final int basenameOffsetOffset = 0x10; 172 final int networkVolumeTableOffsetOffset = 0x14; 173 final int finalnameOffsetOffset = 0x18; 174 int finalnameOffset = link[fileStart + finalnameOffsetOffset] + fileStart; 175 String finalname = getNullDelimitedString(link, finalnameOffset); 176 if (isLocal) { 177 int basenameOffset = link[fileStart + basenameOffsetOffset] + fileStart; 178 String basename = getNullDelimitedString(link, basenameOffset); 179 realFile = basename + finalname; 180 } else { 181 int networkVolumeTableOffset = link[fileStart + networkVolumeTableOffsetOffset] + fileStart; 182 int shareNameOffsetOffset = 0x08; 183 int shareNameOffset = link[networkVolumeTableOffset + shareNameOffsetOffset] 184 + networkVolumeTableOffset; 185 String shareName = getNullDelimitedString(link, shareNameOffset); 186 realFile = shareName + "\\" + finalname; 187 } 188 } catch (ArrayIndexOutOfBoundsException e) { 189 ParseException ex = new ParseException("Could not be parsed, probably not a valid WindowsShortcut", 0); 190 ex.initCause(e); 191 throw ex; 192 } 193 } 194 195 private static String getNullDelimitedString(byte[] bytes, int off) { 196 int len = 0; 197 // count bytes until the null character (0) 198 while (true) { 199 if (bytes[off + len] == 0) { 200 break; 201 } 202 len++; 203 } 204 return new String(bytes, off, len, StandardCharsets.UTF_8); 205 } 206 207 /* 208 * convert two bytes into a short note, this is little endian because it's for an Intel only OS. 209 */ 210 private static int bytesToWord(byte[] bytes, int off) { 211 return ((bytes[off + 1] & 0xff) << 8) | (bytes[off] & 0xff); 212 } 213 214 private static int bytesToDword(byte[] bytes, int off) { 215 return (bytesToWord(bytes, off + 2) << 16) | bytesToWord(bytes, off); 216 } 217}