···1+/***************************************************************************
2+ * __________ __ ___.
3+ * Open \______ \ ____ ____ | | _\_ |__ _______ ___
4+ * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
5+ * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
6+ * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
7+ * \/ \/ \/ \/ \/
8+ * $Id$
9+ *
10+ * Copyright (C) 2017 Sebastian Leonhardt
11+ *
12+ * This program is free software; you can redistribute it and/or
13+ * modify it under the terms of the GNU General Public License
14+ * as published by the Free Software Foundation; either version 2
15+ * of the License, or (at your option) any later version.
16+ *
17+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
18+ * KIND, either express or implied.
19+ *
20+ ****************************************************************************/
21+22+#include "plugin.h"
23+24+/**
25+ * Follow Windows shortcuts (*.lnk files) in Rockbox.
26+ * If the destination is a file, it will be selected in the file browser,
27+ * a directory will be entered.
28+ * For now, only relative links are supported.
29+ */
30+31+/* a selection of link flags */
32+#define HAS_LINK_TARGET_ID_LIST 0x01
33+#define HAS_LINK_INFO 0x02
34+#define HAS_NAME 0x04
35+#define HAS_RELATIVE_PATH 0x08
36+#define HAS_WORKING_DIR 0x10
37+#define HAS_ARGUMENTS 0x20
38+#define HAS_ICON_LOCATION 0x40
39+#define IS_UNICODE 0x80
40+#define FORCE_NO_LINK_INFO 0x100
41+/* a selection of file attributes flags */
42+#define FILE_ATTRIBUTE_DIRECTORY 0x10
43+#define FILE_ATTRIBUTE_NORMAL 0x80
44+45+46+/**
47+ * Read one byte from file
48+ * \param fd file descriptor
49+ * \param *a where the data should go
50+ * \return false if an error occured, true on success
51+ */
52+static bool read_byte(const int fd, unsigned char *a)
53+{
54+ if (!rb->read(fd, a, 1))
55+ return false;
56+ return true;
57+}
58+59+/**
60+ * Read 16-bit word from file, respecting windows endianness (little endian)
61+ * \param fd file descriptor
62+ * \param *a where the data should go
63+ * \return false if an error occured, true on success
64+ */
65+static bool read_word(const int fd, int *a)
66+{
67+ unsigned char a1,a2;
68+ int r;
69+70+ r = read_byte(fd, &a1);
71+ if (!r)
72+ return false;
73+ r = read_byte(fd, &a2);
74+ if (!r)
75+ return false;
76+ *a = (a2<<8) + a1;
77+ return true;
78+}
79+80+/**
81+ * Read 32-bit word from file, respecting windows endianness (little endian)
82+ * \param fd file descriptor
83+ * \param *a where the data should go
84+ * \return false if an error occured, true on success
85+ */
86+static bool read_lword(const int fd, uint32_t *a)
87+{
88+ int a1,a2;
89+ int r;
90+91+ r = read_word(fd, &a1);
92+ if (!r)
93+ return false;
94+ r = read_word(fd, &a2);
95+ if (!r)
96+ return false;
97+ *a = (a2<<16) + a1;
98+ return true;
99+}
100+101+102+/**
103+ * Scan *.lnk file for relative link target
104+ * \param fd file descriptor
105+ * \param link_target the extracted link destination
106+ * \param target_size available space for the extracted link (in bytes)
107+ * \param link_flags the link flags are stored here
108+ * \param file_atts file attributes are stored here
109+ * \return returns false if extraction failed.
110+ */
111+static bool extract_link_destination(const int fd,
112+ char *link_target, const int target_size,
113+ uint32_t *link_flags, uint32_t *file_atts)
114+{
115+ int r;
116+117+ /* Read ShellLinkHeader */
118+ uint32_t size;
119+ r = read_lword(fd, &size);
120+ if (!r) return false;
121+ if (size!=0x4c) { /* header size MUST be 76 bytes */
122+ DEBUGF("unexpected header size 0x%08lx (must be 0x0000004c)\n", size);
123+ return false;
124+ }
125+126+ /* Skip LinkCLSID (class identifier) */
127+ rb->lseek(fd, 0x10, SEEK_CUR);
128+ /* We need the LinkFlags and File attribute (to see if target is a directory) */
129+ r = read_lword(fd, link_flags);
130+ if (!r) return false;
131+ r = read_lword(fd, file_atts);
132+ if (!r) return false;
133+ rb->lseek(fd, size, SEEK_SET); /* Skip to end of header */
134+135+ /* For now we only support relative links, so we can exit right away
136+ if no relative link structure is present */
137+ if (!(*link_flags & HAS_RELATIVE_PATH)) {
138+ DEBUGF("Link doesn't have relative path information\n");
139+ return false;
140+ }
141+142+ /* Read (skip) LinkTargetIDList structure if present */
143+ if (*link_flags & HAS_LINK_TARGET_ID_LIST) {
144+ int size;
145+ if (!read_word(fd, &size))
146+ return false;
147+ rb->lseek(fd, size, SEEK_CUR);
148+ }
149+150+ /* Read (skip) LinkInfo structure if present */
151+ if (*link_flags & HAS_LINK_INFO) {
152+ uint32_t size;
153+ r = read_lword(fd, &size);
154+ if (!r) return false;
155+ rb->lseek(fd, size-4, SEEK_CUR);
156+ }
157+158+ /* String Data section */
159+160+ /* Read (skip) NAME_STRING StringData structure if present */
161+ if (*link_flags & HAS_NAME) {
162+ int ccount;
163+ if (!read_word(fd, &ccount))
164+ return false;
165+ if (*link_flags & IS_UNICODE)
166+ rb->lseek(fd, ccount*2, SEEK_CUR);
167+ else
168+ rb->lseek(fd, ccount, SEEK_CUR);
169+ }
170+171+ /* Read RELATIVE_PATH StringData structure if present */
172+ /* This is finally the data we are searching for! */
173+ if (*link_flags & HAS_RELATIVE_PATH) {
174+ int ccount;
175+ r = read_word(fd, &ccount);
176+ if (*link_flags & IS_UNICODE) {
177+ unsigned char utf16[4], utf8[10];
178+ link_target[0] = '\0';
179+ for (int i=0; i<ccount; ++i) {
180+ r = read_byte(fd, &utf16[0]);
181+ if (!r) return false;
182+ r = read_byte(fd, &utf16[1]);
183+ if (!r) return false;
184+ /* check for surrogate pair and read the second char */
185+ if (utf16[1] >= 0xD8 && utf16[1] < 0xE0) {
186+ r = read_byte(fd, &utf16[2]);
187+ if (!r) return false;
188+ r = read_byte(fd, &utf16[3]);
189+ if (!r) return false;
190+ ++i;
191+ }
192+ char *ptr = rb->utf16LEdecode(utf16, utf8, 1);
193+ *ptr = '\0';
194+ rb->strlcat(link_target, utf8, target_size);
195+ }
196+ }
197+ else { /* non-unicode */
198+ if (ccount >= target_size) {
199+ DEBUGF("ERROR: link target filename exceeds size!");
200+ return false;
201+ }
202+ rb->read(fd, link_target, ccount);
203+ link_target[ccount] = '\0';
204+ }
205+ }
206+207+ /* convert from windows to unix subdir separators */
208+ for (int i=0; link_target[i] != '\0'; ++i) {
209+ if (link_target[i]=='\\')
210+ link_target[i] = '/';
211+ }
212+213+ return true;
214+}
215+216+217+/**
218+ * strip rightmost part of file/pathname to next '/', i.e. remove filename
219+ * or last subdirectory. Leaves a trailing '/' character.
220+ * \param pathname full path or filename
221+*/
222+static void strip_rightmost_part(char *pathname)
223+{
224+ for (int i = rb->strlen(pathname)-2; i >= 0; --i) {
225+ if (pathname[i] == '/') {
226+ pathname[i+1] = '\0'; /* cut off */
227+ return;
228+ }
229+ }
230+ pathname[0] = '\0';
231+}
232+233+234+/**
235+ * Combine link file's absolute path with relative link target to form
236+ * (absolute) link destination
237+ * \param abs_path full shortcut filename (including path)
238+ * \param rel_path the extracted relative link target
239+ * \param max_len maximum lengt of combined filename
240+ */
241+static void assemble_link_dest(char *const abs_path, char const *rel_path,
242+ const size_t max_len)
243+{
244+ strip_rightmost_part(abs_path); /* cut off link filename */
245+246+ for (;;) {
247+ if (rb->strncmp(rel_path, "../", 3)==0) {
248+ rel_path += 3;
249+ strip_rightmost_part(abs_path);
250+ }
251+ else if (rb->strncmp(rel_path, "./", 2)==0) {
252+ rel_path += 2;
253+ }
254+ else
255+ break;
256+ }
257+258+ if (*rel_path=='/')
259+ ++rel_path; /* avoid double '/' chars when concatenating */
260+ rb->strlcat(abs_path, rel_path, max_len);
261+}
262+263+264+/**
265+ * Select the chosen file in the file browser. A directory (filename ending
266+ * with '/') will be entered.
267+ * \param link_target link target to be selected in the browser
268+ * \return returns false if the target doesn't exist
269+ */
270+static bool goto_entry(char *link_target)
271+{
272+ DEBUGF("Trying to go to '%s'...\n", link_target);
273+ if (!rb->file_exists(link_target))
274+ return false;
275+276+ /* Set the browsers dirfilter to the global setting.
277+ * This is required in case the plugin was launched
278+ * from the plugins browser, in which case the
279+ * dirfilter is set to only display .rock files */
280+ rb->set_dirfilter(rb->global_settings->dirfilter);
281+282+ /* Change directory to the entry selected by the user */
283+ rb->set_current_file(link_target);
284+ return true;
285+}
286+287+288+enum plugin_status plugin_start(const void* void_parameter)
289+{
290+ char *link_filename;
291+ char extracted_link[MAX_PATH];
292+ char link_target[MAX_PATH];
293+ uint32_t lnk_flags;
294+ uint32_t file_atts;
295+296+ /* This is a viewer, so a parameter must have been specified */
297+ if (void_parameter == NULL) {
298+ rb->splash(HZ*3, "No *.lnk file selected");
299+ return PLUGIN_OK;
300+ }
301+302+ link_filename = (char*)void_parameter;
303+ DEBUGF("Shortcut filename: \"%s\"\n", link_filename);
304+305+ int fd = rb->open(link_filename, O_RDONLY);
306+ if (fd < 0) {
307+ DEBUGF("Can't open link file\n");
308+ rb->splashf(HZ*3, "Can't open link file!");
309+ return PLUGIN_OK;
310+ }
311+312+ if (!extract_link_destination(fd, extracted_link, sizeof(extracted_link),
313+ &lnk_flags, &file_atts)) {
314+ rb->close(fd);
315+ DEBUGF("Error in extract_link_destination()\n");
316+ rb->splashf(HZ*3, "Unsupported or erroneous link file format");
317+ return PLUGIN_OK;
318+ }
319+ rb->close(fd);
320+ DEBUGF("Shortcut destination: \"%s\"\n", extracted_link);
321+322+ rb->strcpy(link_target, link_filename);
323+ assemble_link_dest(link_target, extracted_link, sizeof(link_target));
324+ DEBUGF("Link absolute path: \"%s\"\n", link_target);
325+326+ /* if target is a directory, add '/' to the dir name,
327+ so that the directory gets entered instead of just highlighted */
328+ if (file_atts & FILE_ATTRIBUTE_DIRECTORY)
329+ if (link_target[rb->strlen(link_target)-1] != '/')
330+ rb->strlcat(link_target, "/", sizeof(link_target));
331+332+ if (!goto_entry(link_target)) {
333+ char *what;
334+ if (file_atts & FILE_ATTRIBUTE_DIRECTORY)
335+ what = "directory";
336+ else
337+ what = "file";
338+ rb->splashf(HZ*3, "Can't find %s %s", what, link_target);
339+ DEBUGF("Can't find %s %s", what, link_target);
340+ return PLUGIN_OK;
341+ }
342+343+ return PLUGIN_OK;
344+}
···8jump to. All names should be full absolute names, i.e. they should start
9with a \fname{/}. Directory names should also end with a \fname{/}.
10000011\subsubsection{How to create \fname{.link} files}
1213You can use your favourite text editor to create a \fname{.link} file on the
···8jump to. All names should be full absolute names, i.e. they should start
9with a \fname{/}. Directory names should also end with a \fname{/}.
1011+\note{This plugin cannot read Microsoft Windows shortcuts (\fname{.lnk}
12+files). These are handled by a separate plugin; see
13+\reference{ref:Winshortcutsplugin}.}
14+15\subsubsection{How to create \fname{.link} files}
1617You can use your favourite text editor to create a \fname{.link} file on the
+15
manual/plugins/winshortcuts.tex
···000000000000000
···1+\subsection{Windows Shortcuts}
2+\label{ref:Winshortcutsplugin}
3+4+This plugin follows Microsoft Windows Explorer shortcuts (\fname{.lnk} files).
5+In Rockbox, these types of shortcuts will show up as \fname{.lnk} files. To
6+follow a shortcut, just ``play'' a \fname{.lnk} file from the file browser.
7+The plugin will navigate the file browser to the linked file (which
8+will be highlighted) or directory (which will be opened). Linked files will
9+not be automatically opened; you must do this manually.
10+11+Only relative links across the same volume are supported.
12+13+\note{You may like to use native Rockbox shortcuts instead. These can be
14+ created from within Rockbox itself and have advanced capabilities.
15+ See \reference{ref:Shortcutsplugin}.}