···11+/***************************************************************************
22+ * __________ __ ___.
33+ * Open \______ \ ____ ____ | | _\_ |__ _______ ___
44+ * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
55+ * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
66+ * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
77+ * \/ \/ \/ \/ \/
88+ * $Id$
99+ *
1010+ * Copyright (C) 2017 Sebastian Leonhardt
1111+ *
1212+ * This program is free software; you can redistribute it and/or
1313+ * modify it under the terms of the GNU General Public License
1414+ * as published by the Free Software Foundation; either version 2
1515+ * of the License, or (at your option) any later version.
1616+ *
1717+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
1818+ * KIND, either express or implied.
1919+ *
2020+ ****************************************************************************/
2121+2222+#include "plugin.h"
2323+2424+/**
2525+ * Follow Windows shortcuts (*.lnk files) in Rockbox.
2626+ * If the destination is a file, it will be selected in the file browser,
2727+ * a directory will be entered.
2828+ * For now, only relative links are supported.
2929+ */
3030+3131+/* a selection of link flags */
3232+#define HAS_LINK_TARGET_ID_LIST 0x01
3333+#define HAS_LINK_INFO 0x02
3434+#define HAS_NAME 0x04
3535+#define HAS_RELATIVE_PATH 0x08
3636+#define HAS_WORKING_DIR 0x10
3737+#define HAS_ARGUMENTS 0x20
3838+#define HAS_ICON_LOCATION 0x40
3939+#define IS_UNICODE 0x80
4040+#define FORCE_NO_LINK_INFO 0x100
4141+/* a selection of file attributes flags */
4242+#define FILE_ATTRIBUTE_DIRECTORY 0x10
4343+#define FILE_ATTRIBUTE_NORMAL 0x80
4444+4545+4646+/**
4747+ * Read one byte from file
4848+ * \param fd file descriptor
4949+ * \param *a where the data should go
5050+ * \return false if an error occured, true on success
5151+ */
5252+static bool read_byte(const int fd, unsigned char *a)
5353+{
5454+ if (!rb->read(fd, a, 1))
5555+ return false;
5656+ return true;
5757+}
5858+5959+/**
6060+ * Read 16-bit word from file, respecting windows endianness (little endian)
6161+ * \param fd file descriptor
6262+ * \param *a where the data should go
6363+ * \return false if an error occured, true on success
6464+ */
6565+static bool read_word(const int fd, int *a)
6666+{
6767+ unsigned char a1,a2;
6868+ int r;
6969+7070+ r = read_byte(fd, &a1);
7171+ if (!r)
7272+ return false;
7373+ r = read_byte(fd, &a2);
7474+ if (!r)
7575+ return false;
7676+ *a = (a2<<8) + a1;
7777+ return true;
7878+}
7979+8080+/**
8181+ * Read 32-bit word from file, respecting windows endianness (little endian)
8282+ * \param fd file descriptor
8383+ * \param *a where the data should go
8484+ * \return false if an error occured, true on success
8585+ */
8686+static bool read_lword(const int fd, uint32_t *a)
8787+{
8888+ int a1,a2;
8989+ int r;
9090+9191+ r = read_word(fd, &a1);
9292+ if (!r)
9393+ return false;
9494+ r = read_word(fd, &a2);
9595+ if (!r)
9696+ return false;
9797+ *a = (a2<<16) + a1;
9898+ return true;
9999+}
100100+101101+102102+/**
103103+ * Scan *.lnk file for relative link target
104104+ * \param fd file descriptor
105105+ * \param link_target the extracted link destination
106106+ * \param target_size available space for the extracted link (in bytes)
107107+ * \param link_flags the link flags are stored here
108108+ * \param file_atts file attributes are stored here
109109+ * \return returns false if extraction failed.
110110+ */
111111+static bool extract_link_destination(const int fd,
112112+ char *link_target, const int target_size,
113113+ uint32_t *link_flags, uint32_t *file_atts)
114114+{
115115+ int r;
116116+117117+ /* Read ShellLinkHeader */
118118+ uint32_t size;
119119+ r = read_lword(fd, &size);
120120+ if (!r) return false;
121121+ if (size!=0x4c) { /* header size MUST be 76 bytes */
122122+ DEBUGF("unexpected header size 0x%08lx (must be 0x0000004c)\n", size);
123123+ return false;
124124+ }
125125+126126+ /* Skip LinkCLSID (class identifier) */
127127+ rb->lseek(fd, 0x10, SEEK_CUR);
128128+ /* We need the LinkFlags and File attribute (to see if target is a directory) */
129129+ r = read_lword(fd, link_flags);
130130+ if (!r) return false;
131131+ r = read_lword(fd, file_atts);
132132+ if (!r) return false;
133133+ rb->lseek(fd, size, SEEK_SET); /* Skip to end of header */
134134+135135+ /* For now we only support relative links, so we can exit right away
136136+ if no relative link structure is present */
137137+ if (!(*link_flags & HAS_RELATIVE_PATH)) {
138138+ DEBUGF("Link doesn't have relative path information\n");
139139+ return false;
140140+ }
141141+142142+ /* Read (skip) LinkTargetIDList structure if present */
143143+ if (*link_flags & HAS_LINK_TARGET_ID_LIST) {
144144+ int size;
145145+ if (!read_word(fd, &size))
146146+ return false;
147147+ rb->lseek(fd, size, SEEK_CUR);
148148+ }
149149+150150+ /* Read (skip) LinkInfo structure if present */
151151+ if (*link_flags & HAS_LINK_INFO) {
152152+ uint32_t size;
153153+ r = read_lword(fd, &size);
154154+ if (!r) return false;
155155+ rb->lseek(fd, size-4, SEEK_CUR);
156156+ }
157157+158158+ /* String Data section */
159159+160160+ /* Read (skip) NAME_STRING StringData structure if present */
161161+ if (*link_flags & HAS_NAME) {
162162+ int ccount;
163163+ if (!read_word(fd, &ccount))
164164+ return false;
165165+ if (*link_flags & IS_UNICODE)
166166+ rb->lseek(fd, ccount*2, SEEK_CUR);
167167+ else
168168+ rb->lseek(fd, ccount, SEEK_CUR);
169169+ }
170170+171171+ /* Read RELATIVE_PATH StringData structure if present */
172172+ /* This is finally the data we are searching for! */
173173+ if (*link_flags & HAS_RELATIVE_PATH) {
174174+ int ccount;
175175+ r = read_word(fd, &ccount);
176176+ if (*link_flags & IS_UNICODE) {
177177+ unsigned char utf16[4], utf8[10];
178178+ link_target[0] = '\0';
179179+ for (int i=0; i<ccount; ++i) {
180180+ r = read_byte(fd, &utf16[0]);
181181+ if (!r) return false;
182182+ r = read_byte(fd, &utf16[1]);
183183+ if (!r) return false;
184184+ /* check for surrogate pair and read the second char */
185185+ if (utf16[1] >= 0xD8 && utf16[1] < 0xE0) {
186186+ r = read_byte(fd, &utf16[2]);
187187+ if (!r) return false;
188188+ r = read_byte(fd, &utf16[3]);
189189+ if (!r) return false;
190190+ ++i;
191191+ }
192192+ char *ptr = rb->utf16LEdecode(utf16, utf8, 1);
193193+ *ptr = '\0';
194194+ rb->strlcat(link_target, utf8, target_size);
195195+ }
196196+ }
197197+ else { /* non-unicode */
198198+ if (ccount >= target_size) {
199199+ DEBUGF("ERROR: link target filename exceeds size!");
200200+ return false;
201201+ }
202202+ rb->read(fd, link_target, ccount);
203203+ link_target[ccount] = '\0';
204204+ }
205205+ }
206206+207207+ /* convert from windows to unix subdir separators */
208208+ for (int i=0; link_target[i] != '\0'; ++i) {
209209+ if (link_target[i]=='\\')
210210+ link_target[i] = '/';
211211+ }
212212+213213+ return true;
214214+}
215215+216216+217217+/**
218218+ * strip rightmost part of file/pathname to next '/', i.e. remove filename
219219+ * or last subdirectory. Leaves a trailing '/' character.
220220+ * \param pathname full path or filename
221221+*/
222222+static void strip_rightmost_part(char *pathname)
223223+{
224224+ for (int i = rb->strlen(pathname)-2; i >= 0; --i) {
225225+ if (pathname[i] == '/') {
226226+ pathname[i+1] = '\0'; /* cut off */
227227+ return;
228228+ }
229229+ }
230230+ pathname[0] = '\0';
231231+}
232232+233233+234234+/**
235235+ * Combine link file's absolute path with relative link target to form
236236+ * (absolute) link destination
237237+ * \param abs_path full shortcut filename (including path)
238238+ * \param rel_path the extracted relative link target
239239+ * \param max_len maximum lengt of combined filename
240240+ */
241241+static void assemble_link_dest(char *const abs_path, char const *rel_path,
242242+ const size_t max_len)
243243+{
244244+ strip_rightmost_part(abs_path); /* cut off link filename */
245245+246246+ for (;;) {
247247+ if (rb->strncmp(rel_path, "../", 3)==0) {
248248+ rel_path += 3;
249249+ strip_rightmost_part(abs_path);
250250+ }
251251+ else if (rb->strncmp(rel_path, "./", 2)==0) {
252252+ rel_path += 2;
253253+ }
254254+ else
255255+ break;
256256+ }
257257+258258+ if (*rel_path=='/')
259259+ ++rel_path; /* avoid double '/' chars when concatenating */
260260+ rb->strlcat(abs_path, rel_path, max_len);
261261+}
262262+263263+264264+/**
265265+ * Select the chosen file in the file browser. A directory (filename ending
266266+ * with '/') will be entered.
267267+ * \param link_target link target to be selected in the browser
268268+ * \return returns false if the target doesn't exist
269269+ */
270270+static bool goto_entry(char *link_target)
271271+{
272272+ DEBUGF("Trying to go to '%s'...\n", link_target);
273273+ if (!rb->file_exists(link_target))
274274+ return false;
275275+276276+ /* Set the browsers dirfilter to the global setting.
277277+ * This is required in case the plugin was launched
278278+ * from the plugins browser, in which case the
279279+ * dirfilter is set to only display .rock files */
280280+ rb->set_dirfilter(rb->global_settings->dirfilter);
281281+282282+ /* Change directory to the entry selected by the user */
283283+ rb->set_current_file(link_target);
284284+ return true;
285285+}
286286+287287+288288+enum plugin_status plugin_start(const void* void_parameter)
289289+{
290290+ char *link_filename;
291291+ char extracted_link[MAX_PATH];
292292+ char link_target[MAX_PATH];
293293+ uint32_t lnk_flags;
294294+ uint32_t file_atts;
295295+296296+ /* This is a viewer, so a parameter must have been specified */
297297+ if (void_parameter == NULL) {
298298+ rb->splash(HZ*3, "No *.lnk file selected");
299299+ return PLUGIN_OK;
300300+ }
301301+302302+ link_filename = (char*)void_parameter;
303303+ DEBUGF("Shortcut filename: \"%s\"\n", link_filename);
304304+305305+ int fd = rb->open(link_filename, O_RDONLY);
306306+ if (fd < 0) {
307307+ DEBUGF("Can't open link file\n");
308308+ rb->splashf(HZ*3, "Can't open link file!");
309309+ return PLUGIN_OK;
310310+ }
311311+312312+ if (!extract_link_destination(fd, extracted_link, sizeof(extracted_link),
313313+ &lnk_flags, &file_atts)) {
314314+ rb->close(fd);
315315+ DEBUGF("Error in extract_link_destination()\n");
316316+ rb->splashf(HZ*3, "Unsupported or erroneous link file format");
317317+ return PLUGIN_OK;
318318+ }
319319+ rb->close(fd);
320320+ DEBUGF("Shortcut destination: \"%s\"\n", extracted_link);
321321+322322+ rb->strcpy(link_target, link_filename);
323323+ assemble_link_dest(link_target, extracted_link, sizeof(link_target));
324324+ DEBUGF("Link absolute path: \"%s\"\n", link_target);
325325+326326+ /* if target is a directory, add '/' to the dir name,
327327+ so that the directory gets entered instead of just highlighted */
328328+ if (file_atts & FILE_ATTRIBUTE_DIRECTORY)
329329+ if (link_target[rb->strlen(link_target)-1] != '/')
330330+ rb->strlcat(link_target, "/", sizeof(link_target));
331331+332332+ if (!goto_entry(link_target)) {
333333+ char *what;
334334+ if (file_atts & FILE_ATTRIBUTE_DIRECTORY)
335335+ what = "directory";
336336+ else
337337+ what = "file";
338338+ rb->splashf(HZ*3, "Can't find %s %s", what, link_target);
339339+ DEBUGF("Can't find %s %s", what, link_target);
340340+ return PLUGIN_OK;
341341+ }
342342+343343+ return PLUGIN_OK;
344344+}
···88jump to. All names should be full absolute names, i.e. they should start
99with a \fname{/}. Directory names should also end with a \fname{/}.
10101111+\note{This plugin cannot read Microsoft Windows shortcuts (\fname{.lnk}
1212+files). These are handled by a separate plugin; see
1313+\reference{ref:Winshortcutsplugin}.}
1414+1115\subsubsection{How to create \fname{.link} files}
12161317You can use your favourite text editor to create a \fname{.link} file on the
+15
manual/plugins/winshortcuts.tex
···11+\subsection{Windows Shortcuts}
22+\label{ref:Winshortcutsplugin}
33+44+This plugin follows Microsoft Windows Explorer shortcuts (\fname{.lnk} files).
55+In Rockbox, these types of shortcuts will show up as \fname{.lnk} files. To
66+follow a shortcut, just ``play'' a \fname{.lnk} file from the file browser.
77+The plugin will navigate the file browser to the linked file (which
88+will be highlighted) or directory (which will be opened). Linked files will
99+not be automatically opened; you must do this manually.
1010+1111+Only relative links across the same volume are supported.
1212+1313+\note{You may like to use native Rockbox shortcuts instead. These can be
1414+ created from within Rockbox itself and have advanced capabilities.
1515+ See \reference{ref:Shortcutsplugin}.}