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 * Returns the name of the filesystem object pointed to by this shortcut. 071 * @return the name of the filesystem object pointed to by this shortcut 072 */ 073 public String getRealFilename() { 074 return realFile; 075 } 076 077 /** 078 * Tests if the shortcut points to a local resource. 079 * @return true if the 'local' bit is set in this shortcut, false otherwise 080 */ 081 public boolean isLocal() { 082 return isLocal; 083 } 084 085 /** 086 * Tests if the shortcut points to a directory. 087 * @return true if the 'directory' bit is set in this shortcut, false otherwise 088 */ 089 public boolean isDirectory() { 090 return isDirectory; 091 } 092 093 /** 094 * Gets all the bytes from an InputStream 095 * @param in the InputStream from which to read bytes 096 * @return array of all the bytes contained in 'in' 097 * @throws IOException if an IOException is encountered while reading the data from the InputStream 098 */ 099 private static byte[] getBytes(InputStream in) throws IOException { 100 return getBytes(in, null); 101 } 102 103 /** 104 * Gets up to max bytes from an InputStream 105 * @param in the InputStream from which to read bytes 106 * @param max maximum number of bytes to read 107 * @return array of all the bytes contained in 'in' 108 * @throws IOException if an IOException is encountered while reading the data from the InputStream 109 */ 110 private static byte[] getBytes(InputStream in, Integer max) throws IOException { 111 // read the entire file into a byte buffer 112 ByteArrayOutputStream bout = new ByteArrayOutputStream(); 113 byte[] buff = new byte[256]; 114 while (max == null || max > 0) { 115 int n = in.read(buff); 116 if (n == -1) { 117 break; 118 } 119 bout.write(buff, 0, n); 120 if (max != null) 121 max -= n; 122 } 123 in.close(); 124 return bout.toByteArray(); 125 } 126 127 private static boolean isMagicPresent(byte[] link) { 128 final int magic = 0x0000004C; 129 final int magicOffset = 0x00; 130 return link.length >= 32 && bytesToDword(link, magicOffset) == magic; 131 } 132 133 /** 134 * Gobbles up link data by parsing it and storing info in member fields 135 * @param link all the bytes from the .lnk file 136 * @throws ParseException if a parsing error occurs 137 */ 138 private void parseLink(byte[] link) throws ParseException { 139 try { 140 if (!isMagicPresent(link)) 141 throw new ParseException("Invalid shortcut; magic is missing", 0); 142 143 // get the flags byte 144 byte flags = link[0x14]; 145 146 // get the file attributes byte 147 final int fileAttsOffset = 0x18; 148 byte fileAtts = link[fileAttsOffset]; 149 byte isDirMask = (byte) 0x10; 150 if ((fileAtts & isDirMask) != 0) { 151 isDirectory = true; 152 } else { 153 isDirectory = false; 154 } 155 156 // if the shell settings are present, skip them 157 final int shellOffset = 0x4c; 158 final byte hasShellMask = (byte) 0x01; 159 int shellLen = 0; 160 if ((flags & hasShellMask) != 0) { 161 // the plus 2 accounts for the length marker itself 162 shellLen = bytesToWord(link, shellOffset) + 2; 163 } 164 165 // get to the file settings 166 int fileStart = 0x4c + shellLen; 167 168 final int fileLocationInfoFlagOffsetOffset = 0x08; 169 int fileLocationInfoFlag = link[fileStart + fileLocationInfoFlagOffsetOffset]; 170 isLocal = (fileLocationInfoFlag & 2) == 0; 171 // get the local volume and local system values 172 final int basenameOffsetOffset = 0x10; 173 final int networkVolumeTableOffsetOffset = 0x14; 174 final int finalnameOffsetOffset = 0x18; 175 int finalnameOffset = link[fileStart + finalnameOffsetOffset] + fileStart; 176 String finalname = getNullDelimitedString(link, finalnameOffset); 177 if (isLocal) { 178 int basenameOffset = link[fileStart + basenameOffsetOffset] + fileStart; 179 String basename = getNullDelimitedString(link, basenameOffset); 180 realFile = basename + finalname; 181 } else { 182 int networkVolumeTableOffset = link[fileStart + networkVolumeTableOffsetOffset] + fileStart; 183 int shareNameOffsetOffset = 0x08; 184 int shareNameOffset = link[networkVolumeTableOffset + shareNameOffsetOffset] 185 + networkVolumeTableOffset; 186 String shareName = getNullDelimitedString(link, shareNameOffset); 187 realFile = shareName + "\\" + finalname; 188 } 189 } catch (ArrayIndexOutOfBoundsException e) { 190 ParseException ex = new ParseException("Could not be parsed, probably not a valid WindowsShortcut", 0); 191 ex.initCause(e); 192 throw ex; 193 } 194 } 195 196 private static String getNullDelimitedString(byte[] bytes, int off) { 197 int len = 0; 198 // count bytes until the null character (0) 199 while (true) { 200 if (bytes[off + len] == 0) { 201 break; 202 } 203 len++; 204 } 205 return new String(bytes, off, len, StandardCharsets.UTF_8); 206 } 207 208 /* 209 * convert two bytes into a short note, this is little endian because it's for an Intel only OS. 210 */ 211 private static int bytesToWord(byte[] bytes, int off) { 212 return ((bytes[off + 1] & 0xff) << 8) | (bytes[off] & 0xff); 213 } 214 215 private static int bytesToDword(byte[] bytes, int off) { 216 return (bytesToWord(bytes, off + 2) << 16) | bytesToWord(bytes, off); 217 } 218}