1 /* win32 shell tools */
2 
3 /* shell_quote()
4  surround dangerous strings with double quotes.
5  escape quotes and backslashes.
6  dest should be twice as large as source
7   + 2 (for quotes) + 1 for zero byte + 1 for possible endslash */
shell_quote(char * dest,const char * source)8 int shell_quote (char * dest, const char * source) {
9   const char * characters = " &<>|^\t";
10   /* Chars other than command separators are actual only when command
11      interpreter is used */
12   BOOL ech, quote = !(*source); /* quote empty arguments */
13   int escaped = 0;
14   char * dcp = dest;
15   *dcp++ = ' ';
16   while (*source) {
17     quote = quote || strchr(characters,*source);
18     ech = *source == '\\';
19     if (!escaped && *source == '"') *dcp++ = '\\';
20     *dcp++ = *source++;
21     escaped = !escaped && ech;
22   }
23   if (quote) {
24     if (escaped) *dcp++ = '\\'; /* double ending slash */
25     *dcp++ = '"'; *dest = '"'; }
26   *dcp = 0;
27   /* shift string left if no quote was inserted */
28   if (!quote) for (dcp = dest;;dcp++) if (!(*dcp = dcp[1])) break;
29   return dcp - dest;
30 }
31 
32 /*========== shell shortcut resolution ==========*/
33 #include <shlobj.h>
34 
35 /* is_cygwin_symlink based on path.cc from cygwin sources
36    only CYGWIN=winsymlinks is supported (i.e., a symlink is a windows *.lnk
37    file); system file with a cookie is NOT (FIXME!)
38    for win_shortcut_hdr description see also,
39    http://msdn.microsoft.com/en-us/library/dd871305(PROT.10).aspx */
40 
41 static const GUID GUID_shortcut =
42   {0x00021401L, 0, 0, {0xc0, 0, 0, 0, 0, 0, 0, 0x46}};
43 
44 enum {
45   WSH_FLAG_IDLIST = 0x01,
46   WSH_FLAG_FILE = 0x02,
47   WSH_FLAG_DESC = 0x04,
48   WSH_FLAG_RELPATH = 0x08,
49   WSH_FLAG_WD = 0x10,
50   WSH_FLAG_CMDLINE = 0x20,
51   WSH_FLAG_ICON = 0x40
52 };
53 
54 struct win_shortcut_hdr
55   {
56     DWORD size;            /* Header size in bytes.  Must contain 0x4c. */
57     GUID magic;            /* GUID of shortcut files. */
58     DWORD flags;           /* Content flags.  See above. */
59 
60     /* The next fields from attr to icon_no are always set to 0 in Cygwin
61        and U/Win shortcuts. */
62     DWORD attr;            /* Target file attributes. */
63     FILETIME ctime;        /* These filetime items are never touched by the */
64     FILETIME mtime;        /* system, apparently. Values don't matter. */
65     FILETIME atime;
66     DWORD filesize;        /* Target filesize. */
67     DWORD icon_no;         /* Icon number. */
68 
69     DWORD run;             /* Values defined in winuser.h. Use SW_NORMAL. */
70     DWORD hotkey;          /* Hotkey value. Set to 0.  */
71     DWORD dummy[2];        /* Future extension probably. Always 0. */
72   };
73 
74 #define WINSHDRSIZE sizeof(struct win_shortcut_hdr)
75 
76 static int
cmp_shortcut_header(struct win_shortcut_hdr * file_header)77 cmp_shortcut_header (struct win_shortcut_hdr *file_header)
78 {
79   /* A Cygwin or U/Win shortcut only contains a description and a relpath.
80      Cygwin shortcuts also might contain an ITEMIDLIST. The run type is
81      always set to SW_NORMAL. */
82   DWORD * pzero = &file_header->attr;
83   /* FILETIME is two DWORDS so check it as array of DWORDS */
84   while (pzero < &file_header->run && !*pzero++);
85   return file_header->size == WINSHDRSIZE
86       && !memcmp (&file_header->magic, &GUID_shortcut, sizeof GUID_shortcut)
87       && (file_header->flags & ~WSH_FLAG_IDLIST)
88          == (WSH_FLAG_DESC | WSH_FLAG_RELPATH)
89       && pzero >= &file_header->run
90       && file_header->run == SW_NORMAL;
91 }
92 
93 enum cygsym_enum { cygsym_notsym = 0, cygsym_issym, cygsym_err };
94 
95 enum cygsym_enum is_cygwin_symlink (const char * filename);
96 
is_cygwin_symlink(const char * filename)97 enum cygsym_enum is_cygwin_symlink (const char * filename)
98 {
99   HANDLE handle;
100   enum cygsym_enum result = cygsym_err;
101   handle = CreateFile(filename,GENERIC_READ,FILE_SHARE_READ|FILE_SHARE_WRITE,
102                       NULL,OPEN_EXISTING,FILE_FLAG_SEQUENTIAL_SCAN,NULL);
103   if (handle == INVALID_HANDLE_VALUE) return result;
104   do {
105     DWORD got;
106     BY_HANDLE_FILE_INFORMATION finfo;
107     struct win_shortcut_hdr header;
108     if (!GetFileInformationByHandle (handle, &finfo)) break;
109     result = cygsym_notsym;
110     if (!(finfo.dwFileAttributes & FILE_ATTRIBUTE_READONLY)) break;
111     if (GetFileSize (handle, NULL) > 8192) break;
112     if (!ReadFile (handle, &header, WINSHDRSIZE, &got, NULL)) break;
113     if (got != WINSHDRSIZE || !cmp_shortcut_header (&header))
114       break;
115     result = cygsym_issym;
116   } while (0);
117   CloseHandle(handle);
118   return result;
119 }
120 
121 /* We will need "breakthrough" (BT) version of every COM function
122    which could be called on unitialized (in the current thread)
123    COM library  */
124 
125 HRESULT BTCoCreateInstance (REFCLSID rclsid,  LPUNKNOWN pUnkOuter,
126                             DWORD dwClsContext, REFIID riid,
127                             LPVOID * ppv);
128 
BTCoCreateInstance(REFCLSID rclsid,LPUNKNOWN pUnkOuter,DWORD dwClsContext,REFIID riid,LPVOID * ppv)129 HRESULT BTCoCreateInstance (REFCLSID rclsid,  LPUNKNOWN pUnkOuter,
130                             DWORD dwClsContext, REFIID riid,
131                             LPVOID * ppv)
132 {
133   HRESULT result = CoCreateInstance(rclsid, pUnkOuter, dwClsContext, riid, ppv);
134   if (result != CO_E_NOTINITIALIZED || CoInitialize(NULL) != S_OK)
135     return result;
136   return CoCreateInstance(rclsid, pUnkOuter, dwClsContext, riid, ppv);
137 }
138 
139 /* extracts a filename field from windows shortcut
140  > filename: name the shortcut file
141  < resolved: buffer not less than MAX_PATH
142  < result:   true if link was successfully resolved */
resolve_shell_shortcut(LPCSTR filename,LPSTR resolved)143 static BOOL resolve_shell_shortcut (LPCSTR filename, LPSTR resolved) {
144   HRESULT hres;
145   IShellLink* psl;
146   WIN32_FIND_DATA wfd;
147   BOOL result = FALSE;
148   IPersistFile* ppf;
149 
150   /* no matter it's FS error or not cygwin shortcut -
151      probably it should be fixed */
152   if (is_cygwin_symlink(filename) != cygsym_issym) return FALSE;
153   /* Get a pointer to the IShellLink interface. */
154   hres = BTCoCreateInstance(&CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER,
155                             &IID_IShellLink, (LPVOID*)&psl);
156   if (FAILED(hres)) return FALSE;
157   /* Get a pointer to the IPersistFile interface. */
158   hres = psl->lpVtbl->QueryInterface(psl, &IID_IPersistFile,(LPVOID *) &ppf);
159   if (SUCCEEDED(hres)) {
160     WCHAR wsz[MAX_PATH];
161     /* Ensure that the string is Unicode. */
162     MultiByteToWideChar(CP_ACP, 0, filename, -1, wsz,MAX_PATH);
163     /* Load the shortcut. */
164     hres = ppf->lpVtbl->Load(ppf, wsz, STGM_READ);
165     if (SUCCEEDED(hres)) {
166       /* Get the path to the link target. */
167       hres = psl->lpVtbl->GetPath(psl, resolved, MAX_PATH,
168                                   (WIN32_FIND_DATA *)&wfd,
169                                   4 /* SLGP_RAWPATH */);
170       if (SUCCEEDED(hres)) result = TRUE;
171       if (!*resolved) {
172         /* empty string. maybe broken link. try to get description
173            as cygwin stores filenames there */
174         hres = psl->lpVtbl->GetDescription(psl, resolved, MAX_PATH);
175         if (FAILED(hres)) *resolved = 0;
176       }
177     }
178     /* Release the pointer to the IPersistFile interface. */
179     ppf->lpVtbl->Release(ppf);
180   }
181   /* Release the pointer to the IShellLink interface. */
182   psl->lpVtbl->Release(psl);
183   return result;
184 }
185 
186 #if !defined(cpslashp)
187 # define cpslashp(c) ((c) == '\\' || (c) == '/')
188 #endif
189 
190 /* Uses the base from filename to augment pathname.
191    Return false when (pathname is relative AND filename doesn't
192    contain the base), so pathname (contained in shortcut)
193    cannot be referenced other than to current directory
194    what is wrong. */
augment_relative_pathname(LPCSTR filename,LPSTR pathname)195 static BOOL augment_relative_pathname(LPCSTR filename, LPSTR pathname) {
196   /* check if pathname is absolute */
197   /* what to do with "/bar/foo" pathnames ?*/
198   if (cpslashp(pathname[0])) return FALSE; /* let's panic */
199   if (((pathname[0] >= 'a' && pathname[0] <= 'z')
200     || (pathname[0] >= 'A' && pathname[0] <= 'Z'))
201     && pathname[1] == ':' && cpslashp(pathname[2])) return TRUE;
202   if (pathname[0] == '\\' && pathname[1] == '\\') return TRUE;
203   {
204     int fl = strlen(filename);
205     int pl = strlen(pathname);
206     const char * cp = filename + fl - 1;
207     /* find the last slash */
208     for (;!cpslashp(*cp) && cp > filename;cp--);
209     if (!cpslashp(*cp)) return FALSE; /* no slash */
210     memmove(pathname + (cp - filename + 1),pathname,pl + 1);
211     memmove(pathname,filename,cp - filename + 1);
212   }
213   return TRUE;
214 }
215 
216 typedef enum {
217   shell_shortcut_notresolved = 0,
218   shell_shortcut_notexists,
219   shell_shortcut_file,
220   shell_shortcut_directory
221 } shell_shortcut_target_t;
222 
223 /* resolves shortcuts to shortcuts
224  > filename : name of link file to resolve
225  < resolved : buffer to receive resolved name
226  < result : status of resolving and target file attributes */
227 static shell_shortcut_target_t
resolve_shell_shortcut_more(LPCSTR filename,LPSTR resolved)228 resolve_shell_shortcut_more (LPCSTR filename, LPSTR resolved)
229 {
230   char pathname[_MAX_PATH];
231   char pathname1[_MAX_PATH];
232   int dirp = 0;
233   int exists = 0;
234   int try_counter = 33;
235   int l, resolvedp = resolve_shell_shortcut(filename,pathname)
236     && augment_relative_pathname(filename,pathname);
237   /* handle links to links. cygwin can do such */
238   while (resolvedp && try_counter--) {
239     l=strlen(pathname);
240     if (l >= 4 && stricmp(pathname+l-4,".lnk") == 0)
241       resolvedp = resolve_shell_shortcut(pathname,pathname1)
242               && augment_relative_pathname(pathname,pathname1)
243               && strcpy(pathname,pathname1);
244     else {
245     /* not a link to shortcut but can be the symbolic filename */
246       strcpy(pathname+l,".lnk");
247       if (!resolve_shell_shortcut(pathname,pathname1)
248         || !augment_relative_pathname(pathname,pathname1)) {
249         pathname[l] = '\0'; break; }
250       else strcpy(pathname,pathname1);
251     }
252   }
253   if (resolvedp) { /* additional checks */
254     DWORD fileattr = GetFileAttributes(pathname);
255     exists = fileattr != 0xFFFFFFFF;
256     dirp = exists && fileattr&FILE_ATTRIBUTE_DIRECTORY;
257     if (resolved) {
258       strcpy(resolved,pathname);
259       if (dirp) strcat(resolved,"\\");
260     }
261     if (dirp) return shell_shortcut_directory;
262     if (exists && !dirp) return shell_shortcut_file;
263     return shell_shortcut_notexists;
264   } else return shell_shortcut_notresolved;
265 }
266 
267 /* see if a file is normal file or it is a "shell symlink"
268  If directory+filename exists do nothing return false
269  If it doesn't but direstory+filename+".lnk" exists then
270  try to read it. On reading success return true with lnk
271  filename value as resolved. See resolve_shell_shortcut also.
272  > filename: resolving file name
273  < resolved: buffer for resolved path and filename.
274  < result: shell_shortcut_notresolved if file exists or link is invalid.
275            otherwise - shortcut target status */
276 static shell_shortcut_target_t
resolve_shell_symlink(LPCSTR filename,LPSTR resolved)277 resolve_shell_symlink (LPCSTR filename, LPSTR resolved)
278 {
279   char pathname[_MAX_PATH];
280   DWORD fileattr;
281 
282   strcpy(pathname,filename);
283   fileattr = GetFileAttributes(pathname);
284   if (fileattr != 0xFFFFFFFF) return shell_shortcut_notresolved;
285   strcat(pathname,".lnk");
286   fileattr = GetFileAttributes(pathname);
287   if (fileattr == 0xFFFFFFFF) return shell_shortcut_notresolved;
288   return resolve_shell_shortcut_more(pathname,resolved);
289 }
290 
291 /* the ultimate shortcut megaresolver
292    style inspired by directory_search_scandir
293  > namein: absolute filename pointing to file or directory
294             wildcards (only asterisk) may appear only as filename
295  < nameout: filename with directory and file shortcuts resolved
296              on failure holds filename resolved so far
297  < result:  true if resolving succeeded */
real_path(LPCSTR namein,LPSTR nameout)298 BOOL real_path (LPCSTR namein, LPSTR nameout) {
299   WIN32_FIND_DATA wfd;
300   HANDLE h = NULL;
301   char * nametocheck;
302   char * nametocheck_end;
303   int    name_len;
304   /* drive|dir1|dir2|name
305            ^nametocheck
306                ^nametocheck_end */
307   char saved_char;
308   BOOL next_name = 0;/* if we found an lnk and need to start over */
309   int try_counter = 33;
310   if ((name_len = strlen(namein)) >= MAX_PATH) return FALSE;
311   strcpy(nameout,namein);
312   do { /* whole file names */
313     next_name = FALSE;
314     if (!*nameout) return FALSE;
315     /* skip drive or host or first slash */
316     nametocheck = nameout;
317     if (((*nametocheck >= 'a' && *nametocheck <= 'z')
318          || (*nametocheck >= 'A' && *nametocheck <= 'Z'))
319         && nametocheck[1] == ':')
320     { if (cpslashp(nametocheck[2])) {
321         /* drive */
322         nametocheck += 3;
323       } else {
324         /* default directory on drive */
325         char drive[4] = "C:.", *name;
326         int default_len;
327         drive[0] = namein[0];
328         if (!GetFullPathName(drive,_MAX_PATH,nameout,&name)
329             || (default_len = strlen(nameout)) + name_len
330                >= _MAX_PATH) return FALSE;
331         nameout[default_len] = '\\';
332         strcpy(nameout + default_len + 1, namein + 2);
333         name_len += default_len - 1; /* Was C:lisp.exe
334                                         Now C:\clisp\lisp.exe
335                                         removed 2
336                                         added default_len + 1 chars */
337         nametocheck += default_len + 1;
338     } }
339     else if (nametocheck[0]=='\\' && nametocheck[1]=='\\') {
340       int i;
341       /* host */
342       nametocheck+=2;
343       for (i=0;i<2;i++) {/* skip host and sharename */
344         while (*nametocheck && !cpslashp(*nametocheck))
345           nametocheck++;
346         if (*nametocheck) nametocheck++; else return FALSE;
347       }
348     } else if (cpslashp(*nametocheck)) nametocheck++;
349     /* prefix skipped; start checking */
350     do { /* each component after just skipped */
351       int dots_only = 0;
352       int have_stars = 0;
353       /* separate a component */
354       for (nametocheck_end = nametocheck;
355            *nametocheck_end && !cpslashp(*nametocheck_end);
356            nametocheck_end++);
357       if (*nametocheck_end && nametocheck_end == nametocheck)
358         return FALSE;/* two slashes one after another */
359       /* save slash or zero */
360       saved_char = *nametocheck_end;
361       *nametocheck_end = 0;
362       /* Is it . or .. ? FFF handles this strange way */
363       { char * cp = nametocheck;
364         for (;*cp=='.';cp++);
365         dots_only = !(*cp) && cp > nametocheck; }
366       /* Asterisks in the middle of filename: error
367          Asterisks as pathname: success */
368       { char * cp = nametocheck;
369         for (;*cp && *cp!='*';cp++);
370         have_stars = *cp == '*'; }
371       if (have_stars && saved_char) return FALSE;
372       if (!have_stars) {
373         if (dots_only || !*nametocheck) {
374           /* treat 'start/./end', 'drive/', 'host/' specially */
375           /* search for ....\.\* */
376           char saved[2];
377           if (nametocheck_end - nameout + 2 > MAX_PATH) return FALSE;
378           saved[0] = nametocheck_end[1]; saved[1] = nametocheck_end[2];
379           /* !*nametocheck here means there was "something\" before */
380           strcpy(nametocheck_end,*nametocheck?"\\*":"*");
381           h = FindFirstFile(nameout,&wfd);
382           nametocheck_end[1] = saved[0]; nametocheck_end[2] = saved[1];
383           nametocheck_end[0] = 0;
384           if (h != INVALID_HANDLE_VALUE) {
385             FindClose(h); /* don't substitute */
386           } else return FALSE; /* don't try lnk */
387         } else {/* not only dots */
388           h = FindFirstFile(nameout,&wfd);
389           if (h != INVALID_HANDLE_VALUE) {
390             /* make space for full (non 8.3) name component */
391             int     l = strlen(wfd.cFileName),
392                  oldl = nametocheck_end - nametocheck,
393                  new_name_len = name_len + l - oldl;
394             FindClose(h);
395             if (new_name_len >= _MAX_PATH) return FALSE;
396             if (l != oldl) {
397               int restlen =
398                 saved_char?(name_len - (nametocheck_end - nameout)):0;
399               memmove(nametocheck+l,nametocheck_end,restlen+1);
400             }
401             strncpy(nametocheck,wfd.cFileName,l);
402             nametocheck_end = nametocheck + l;
403             name_len = new_name_len;
404           } else {/* try shortcut
405                      Note: something\cyglink.lnk doesn't resolve to the contents
406                            of cyglink.lnk so one can read/write symlink .lnk
407                            files although they are not present in DIRECTORY output.
408                            Is it bug or feature? */
409             char saved[4];
410             char resolved[MAX_PATH];
411             int  resolved_len;
412             shell_shortcut_target_t rresult;
413             if (nametocheck_end - nameout + 4 > MAX_PATH) return FALSE;
414             strncpy(saved,nametocheck_end+1,4);
415             strncpy(nametocheck_end,".lnk",5);
416             rresult = resolve_shell_shortcut_more(nameout,resolved);
417             strncpy(nametocheck_end+1,saved,4);
418             *nametocheck_end = 0;
419             /* use saved_char as directory indicator */
420             if (rresult == shell_shortcut_notresolved
421                 || rresult == shell_shortcut_notexists
422                 || (saved_char ? rresult == shell_shortcut_file
423                     : rresult == shell_shortcut_directory))
424               return FALSE;
425             resolved_len = strlen(resolved);
426             if (saved_char) {
427               /*need to subst nameout..nametocheck-1 with resolved path */
428               int l2 = name_len - (nametocheck_end - nameout);
429               if (resolved_len + l2 + 2 > MAX_PATH) return FALSE;
430               strncpy(resolved + resolved_len, nametocheck_end + 1, l2);
431               name_len = l2 - 1;
432             }
433             name_len += resolved_len;
434             strcpy(nameout,resolved);
435             next_name = TRUE;
436           }
437         }
438       }
439       if (!next_name) {
440         *nametocheck_end = saved_char;
441         nametocheck = nametocheck_end;
442         if (*nametocheck) nametocheck++;
443       }
444     } while (!next_name && *nametocheck);
445     if (!(--try_counter)) return FALSE;
446   } while (next_name);
447   return TRUE;
448 }
449