Class |
Line # |
Actions |
|||
---|---|---|---|---|---|
WindowsShortcut | 30 | 71 | 22 |
1 | package org.stackoverflowusers.file; | |
2 | ||
3 | import java.io.ByteArrayOutputStream; | |
4 | import java.io.File; | |
5 | import java.io.FileInputStream; | |
6 | import java.io.IOException; | |
7 | import java.io.InputStream; | |
8 | import java.text.ParseException; | |
9 | import java.util.Locale; | |
10 | ||
11 | /** | |
12 | * Represents a Windows shortcut (typically visible to Java only as a '.lnk' | |
13 | * file). | |
14 | * | |
15 | * Retrieved 2011-09-23 from | |
16 | * http://stackoverflow.com/questions/309495/windows-shortcut-lnk-parser-in-java/672775#672775 | |
17 | * Originally called LnkParser | |
18 | * | |
19 | * Written by: (the stack overflow users, obviously!) Apache Commons VFS | |
20 | * dependency removed by crysxd (why were we using that!?) | |
21 | * https://github.com/crysxd Headerified, refactored and commented by Code Bling | |
22 | * http://stackoverflow.com/users/675721/code-bling Network file support added | |
23 | * by Stefan Cordes http://stackoverflow.com/users/81330/stefan-cordes Adapted | |
24 | * by Sam Brightman http://stackoverflow.com/users/2492/sam-brightman Based on | |
25 | * information in 'The Windows Shortcut File Format' by Jesse Hager | |
26 | * <jessehager@iname.com> And somewhat based on code from the book 'Swing | |
27 | * Hacks: Tips and Tools for Killer GUIs' by Joshua Marinacci and Chris Adamson | |
28 | * ISBN: 0-596-00907-0 http://www.oreilly.com/catalog/swinghks/ | |
29 | */ | |
30 | public class WindowsShortcut | |
31 | { | |
32 | private boolean isDirectory; | |
33 | ||
34 | private boolean isLocal; | |
35 | ||
36 | private String real_file; | |
37 | ||
38 | /** | |
39 | * Provides a quick test to see if this could be a valid link ! If you try to | |
40 | * instantiate a new WindowShortcut and the link is not valid, Exceptions may | |
41 | * be thrown and Exceptions are extremely slow to generate, therefore any code | |
42 | * needing to loop through several files should first check this. | |
43 | * | |
44 | * @param file | |
45 | * the potential link | |
46 | * @return true if may be a link, false otherwise | |
47 | * @throws IOException | |
48 | * if an IOException is thrown while reading from the file | |
49 | */ | |
50 | 0 | public static boolean isPotentialValidLink(File file) throws IOException |
51 | { | |
52 | 0 | final int minimum_length = 0x64; |
53 | 0 | InputStream fis = new FileInputStream(file); |
54 | 0 | boolean isPotentiallyValid = false; |
55 | 0 | try |
56 | { | |
57 | 0 | isPotentiallyValid = file.isFile() |
58 | && file.getName().toLowerCase(Locale.ROOT).endsWith(".lnk") | |
59 | && fis.available() >= minimum_length | |
60 | && isMagicPresent(getBytes(fis, 32)); | |
61 | } finally | |
62 | { | |
63 | 0 | fis.close(); |
64 | } | |
65 | 0 | return isPotentiallyValid; |
66 | } | |
67 | ||
68 | 0 | public WindowsShortcut(File file) throws IOException, ParseException |
69 | { | |
70 | 0 | InputStream in = new FileInputStream(file); |
71 | 0 | try |
72 | { | |
73 | 0 | parseLink(getBytes(in)); |
74 | } finally | |
75 | { | |
76 | 0 | in.close(); |
77 | } | |
78 | } | |
79 | ||
80 | /** | |
81 | * @return the name of the filesystem object pointed to by this shortcut | |
82 | */ | |
83 | 0 | public String getRealFilename() |
84 | { | |
85 | 0 | return real_file; |
86 | } | |
87 | ||
88 | /** | |
89 | * Tests if the shortcut points to a local resource. | |
90 | * | |
91 | * @return true if the 'local' bit is set in this shortcut, false otherwise | |
92 | */ | |
93 | 0 | public boolean isLocal() |
94 | { | |
95 | 0 | return isLocal; |
96 | } | |
97 | ||
98 | /** | |
99 | * Tests if the shortcut points to a directory. | |
100 | * | |
101 | * @return true if the 'directory' bit is set in this shortcut, false | |
102 | * otherwise | |
103 | */ | |
104 | 0 | public boolean isDirectory() |
105 | { | |
106 | 0 | return isDirectory; |
107 | } | |
108 | ||
109 | /** | |
110 | * Gets all the bytes from an InputStream | |
111 | * | |
112 | * @param in | |
113 | * the InputStream from which to read bytes | |
114 | * @return array of all the bytes contained in 'in' | |
115 | * @throws IOException | |
116 | * if an IOException is encountered while reading the data from the | |
117 | * InputStream | |
118 | */ | |
119 | 0 | private static byte[] getBytes(InputStream in) throws IOException |
120 | { | |
121 | 0 | return getBytes(in, null); |
122 | } | |
123 | ||
124 | /** | |
125 | * Gets up to max bytes from an InputStream | |
126 | * | |
127 | * @param in | |
128 | * the InputStream from which to read bytes | |
129 | * @param max | |
130 | * maximum number of bytes to read | |
131 | * @return array of all the bytes contained in 'in' | |
132 | * @throws IOException | |
133 | * if an IOException is encountered while reading the data from the | |
134 | * InputStream | |
135 | */ | |
136 | 0 | private static byte[] getBytes(InputStream in, Integer max) |
137 | throws IOException | |
138 | { | |
139 | // read the entire file into a byte buffer | |
140 | 0 | ByteArrayOutputStream bout = new ByteArrayOutputStream(); |
141 | 0 | byte[] buff = new byte[256]; |
142 | 0 | while (max == null || max > 0) |
143 | { | |
144 | 0 | int n = in.read(buff); |
145 | 0 | if (n == -1) |
146 | { | |
147 | 0 | break; |
148 | } | |
149 | 0 | bout.write(buff, 0, n); |
150 | 0 | if (max != null) |
151 | 0 | max -= n; |
152 | } | |
153 | 0 | in.close(); |
154 | 0 | return bout.toByteArray(); |
155 | } | |
156 | ||
157 | 0 | private static boolean isMagicPresent(byte[] link) |
158 | { | |
159 | 0 | final int magic = 0x0000004C; |
160 | 0 | final int magic_offset = 0x00; |
161 | 0 | return link.length >= 32 && bytesToDword(link, magic_offset) == magic; |
162 | } | |
163 | ||
164 | /** | |
165 | * Gobbles up link data by parsing it and storing info in member fields | |
166 | * | |
167 | * @param link | |
168 | * all the bytes from the .lnk file | |
169 | */ | |
170 | 0 | private void parseLink(byte[] link) throws ParseException |
171 | { | |
172 | 0 | try |
173 | { | |
174 | 0 | if (!isMagicPresent(link)) |
175 | 0 | throw new ParseException("Invalid shortcut; magic is missing", 0); |
176 | ||
177 | // get the flags byte | |
178 | 0 | byte flags = link[0x14]; |
179 | ||
180 | // get the file attributes byte | |
181 | 0 | final int file_atts_offset = 0x18; |
182 | 0 | byte file_atts = link[file_atts_offset]; |
183 | 0 | byte is_dir_mask = (byte) 0x10; |
184 | 0 | if ((file_atts & is_dir_mask) > 0) |
185 | { | |
186 | 0 | isDirectory = true; |
187 | } | |
188 | else | |
189 | { | |
190 | 0 | isDirectory = false; |
191 | } | |
192 | ||
193 | // if the shell settings are present, skip them | |
194 | 0 | final int shell_offset = 0x4c; |
195 | 0 | final byte has_shell_mask = (byte) 0x01; |
196 | 0 | int shell_len = 0; |
197 | 0 | if ((flags & has_shell_mask) > 0) |
198 | { | |
199 | // the plus 2 accounts for the length marker itself | |
200 | 0 | shell_len = bytesToWord(link, shell_offset) + 2; |
201 | } | |
202 | ||
203 | // get to the file settings | |
204 | 0 | int file_start = 0x4c + shell_len; |
205 | ||
206 | 0 | final int file_location_info_flag_offset_offset = 0x08; |
207 | 0 | int file_location_info_flag = link[file_start |
208 | + file_location_info_flag_offset_offset]; | |
209 | 0 | isLocal = (file_location_info_flag & 2) == 0; |
210 | // get the local volume and local system values | |
211 | // final int localVolumeTable_offset_offset = 0x0C; | |
212 | 0 | final int basename_offset_offset = 0x10; |
213 | 0 | final int networkVolumeTable_offset_offset = 0x14; |
214 | 0 | final int finalname_offset_offset = 0x18; |
215 | 0 | int finalname_offset = link[file_start + finalname_offset_offset] |
216 | + file_start; | |
217 | 0 | String finalname = getNullDelimitedString(link, finalname_offset); |
218 | 0 | if (isLocal) |
219 | { | |
220 | 0 | int basename_offset = link[file_start + basename_offset_offset] |
221 | + file_start; | |
222 | 0 | String basename = getNullDelimitedString(link, basename_offset); |
223 | 0 | real_file = basename + finalname; |
224 | } | |
225 | else | |
226 | { | |
227 | 0 | int networkVolumeTable_offset = link[file_start |
228 | + networkVolumeTable_offset_offset] + file_start; | |
229 | 0 | int shareName_offset_offset = 0x08; |
230 | 0 | int shareName_offset = link[networkVolumeTable_offset |
231 | + shareName_offset_offset] + networkVolumeTable_offset; | |
232 | 0 | String shareName = getNullDelimitedString(link, shareName_offset); |
233 | 0 | real_file = shareName + "\\" + finalname; |
234 | } | |
235 | } catch (ArrayIndexOutOfBoundsException e) | |
236 | { | |
237 | 0 | throw new ParseException( |
238 | "Could not be parsed, probably not a valid WindowsShortcut", | |
239 | 0); | |
240 | } | |
241 | } | |
242 | ||
243 | 0 | private static String getNullDelimitedString(byte[] bytes, int off) |
244 | { | |
245 | 0 | int len = 0; |
246 | // count bytes until the null character (0) | |
247 | 0 | while (true) |
248 | { | |
249 | 0 | if (bytes[off + len] == 0) |
250 | { | |
251 | 0 | break; |
252 | } | |
253 | 0 | len++; |
254 | } | |
255 | 0 | return new String(bytes, off, len); |
256 | } | |
257 | ||
258 | /* | |
259 | * convert two bytes into a short note, this is little endian because it's | |
260 | * for an Intel only OS. | |
261 | */ | |
262 | 0 | private static int bytesToWord(byte[] bytes, int off) |
263 | { | |
264 | 0 | return ((bytes[off + 1] & 0xff) << 8) | (bytes[off] & 0xff); |
265 | } | |
266 | ||
267 | 0 | private static int bytesToDword(byte[] bytes, int off) |
268 | { | |
269 | 0 | return (bytesToWord(bytes, off + 2) << 16) | bytesToWord(bytes, off); |
270 | } | |
271 | ||
272 | } |