lnk/
lib.rs

1#![allow(unexpected_cfgs)]
2#![warn(missing_docs)]
3
4//! # Shell Link parser and writer for Rust.
5//!
6//! Works on any OS - although only really useful in Windows, this library can parse and write
7//! .lnk files, a shell link, that can be understood by Windows.
8//!
9//! To get started, see the [ShellLink](struct.ShellLink.html) struct.
10//!
11//! The full specification of these files can be found at
12//! [Microsoft's Website](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-shllink/16cb4ca1-9339-4d0c-a68d-bf1d6cc0f943).
13//!
14//! ## Read Example
15//!
16//! A simple example appears as follows:
17//! ```
18//! use lnk::ShellLink;
19//! use lnk::encoding::WINDOWS_1252;
20//! // ...
21//! let shortcut = lnk::ShellLink::open("tests/data/test.lnk", WINDOWS_1252).unwrap();
22//! println!("{:#?}", shortcut);
23//! ```
24//!
25//! ## Write Example
26//!
27//! A simple example appears as follows:
28//! ```ignore
29//! use lnk::ShellLink;
30//! // ...
31//! ShellLink::new_simple(std::path::Path::new(r"C:\Windows\System32\notepad.exe"));
32//! ```
33//!
34//! > **IMPORTANT!**: Writing capability is currently in a very early stage and probably won't work!
35
36use binrw::BinReaderExt;
37use getset::{Getters, MutGetters};
38#[allow(unused)]
39use log::{debug, error, info, trace, warn};
40#[cfg(feature = "serde")]
41use serde::Serialize;
42
43use std::io::BufReader;
44#[cfg(feature = "binwrite")]
45use std::io::BufWriter;
46use std::path::Path;
47use std::{fs::File, io::Seek};
48
49mod header;
50pub use header::{
51    FileAttributeFlags, HotkeyFlags, HotkeyKey, HotkeyModifiers, LinkFlags, ShellLinkHeader,
52    ShowCommand,
53};
54
55/// The LinkTargetIDList structure specifies the target of the link. The presence of this optional
56/// structure is specified by the HasLinkTargetIDList bit (LinkFlagssection 2.1.1) in the
57/// ShellLinkHeader(section2.1).
58pub mod linktarget;
59pub use linktarget::LinkTargetIdList;
60
61/// The LinkInfo structure specifies information necessary to resolve a
62/// linktarget if it is not found in its original location. This includes
63/// information about the volume that the target was stored on, the mapped
64/// drive letter, and a Universal Naming Convention (UNC)form of the path
65/// if one existed when the linkwas created. For more details about UNC
66/// paths, see [MS-DFSNM] section 2.2.1.4
67pub mod linkinfo;
68pub use linkinfo::LinkInfo;
69
70mod stringdata;
71pub use stringdata::StringData;
72
73/// Structures from the ExtraData section of the Shell Link.
74pub mod extradata;
75pub use extradata::ExtraData;
76
77mod generic_types;
78pub use generic_types::filetime::FileTime;
79pub use generic_types::guid::*;
80pub use generic_types::idlist::*;
81
82mod current_offset;
83pub use current_offset::*;
84
85mod strings;
86pub use strings::*;
87
88mod itemid;
89pub use itemid::*;
90
91#[macro_use]
92mod binread_flags;
93
94mod error;
95pub use error::Error;
96
97/// A shell link
98#[derive(Debug, Getters, MutGetters)]
99#[cfg_attr(feature = "serde", derive(Serialize))]
100#[getset(get = "pub", get_mut = "pub")]
101pub struct ShellLink {
102    /// returns the [`ShellLinkHeader`] structure
103    header: header::ShellLinkHeader,
104
105    /// returns the [`LinkTargetIdList`] structure
106    #[cfg_attr(feature = "serde", serde(skip))]
107    linktarget_id_list: Option<linktarget::LinkTargetIdList>,
108
109    /// returns the [`LinkInfo`] structure
110    link_info: Option<linkinfo::LinkInfo>,
111
112    /// returns the [`StringData`] structure
113    string_data: StringData,
114
115    /// returns the [`ExtraData`] structure
116    #[allow(unused)]
117    extra_data: extradata::ExtraData,
118
119    /// encoding used for this link
120    #[serde(skip)]
121    #[getset(skip)]
122    encoding: &'static encoding_rs::Encoding,
123}
124
125impl Default for ShellLink {
126    /// Create a new ShellLink, left blank for manual configuration.
127    /// For those who are not familar with the Shell Link specification, I
128    /// suggest you look at the [`ShellLink::new_simple`] method.
129    fn default() -> Self {
130        let header = header::ShellLinkHeader::default();
131        let encoding = if header.link_flags().contains(LinkFlags::IS_UNICODE) {
132            encoding_rs::UTF_16LE
133        } else {
134            encoding_rs::WINDOWS_1252
135        };
136        Self {
137            header,
138            linktarget_id_list: None,
139            link_info: None,
140            string_data: Default::default(),
141            extra_data: Default::default(),
142            encoding,
143        }
144    }
145}
146
147impl ShellLink {
148    /// Create a new ShellLink pointing to a location, with otherwise default settings.
149    pub fn new_simple<P: AsRef<Path>>(to: P) -> std::io::Result<Self> {
150        use std::fs;
151        use std::path::PathBuf;
152
153        let meta = fs::metadata(&to)?;
154        let mut canonical = fs::canonicalize(&to)?.into_boxed_path();
155        if cfg!(windows) {
156            // Remove symbol for long path if present.
157            let can_os = canonical.as_os_str().to_str().unwrap();
158            if let Some(stripped) = can_os.strip_prefix("\\\\?\\") {
159                canonical = PathBuf::new().join(stripped).into_boxed_path();
160            }
161        }
162
163        let mut sl = Self::default();
164
165        if meta.is_dir() {
166            sl.header_mut()
167                .set_file_attributes(FileAttributeFlags::FILE_ATTRIBUTE_DIRECTORY);
168        } else {
169            sl.set_relative_path(Some(format!(
170                ".\\{}",
171                canonical.file_name().unwrap().to_str().unwrap()
172            )));
173            sl.set_working_dir(Some(
174                canonical.parent().unwrap().to_str().unwrap().to_string(),
175            ));
176        }
177
178        Ok(sl)
179    }
180
181    /// change the encoding for this link
182    pub fn with_encoding(mut self, encoding: &StringEncoding) -> Self {
183        match encoding {
184            StringEncoding::Unicode => {
185                self.header
186                    .link_flags_mut()
187                    .set(LinkFlags::IS_UNICODE, true);
188                self.encoding = encoding_rs::UTF_16LE;
189            }
190            StringEncoding::CodePage(cp) => {
191                self.header
192                    .link_flags_mut()
193                    .set(LinkFlags::IS_UNICODE, false);
194                self.encoding = cp;
195            }
196        }
197        self
198    }
199
200    /// Save a shell link.
201    ///
202    /// Note that this doesn't save any [`ExtraData`](struct.ExtraData.html) entries.
203    #[cfg(feature = "binwrite")]
204    #[cfg_attr(feature = "binwrite", stability::unstable(feature = "save"))]
205    pub fn save<P: AsRef<std::path::Path>>(&self, path: P) -> Result<(), Error> {
206        use binrw::BinWrite;
207
208        let mut w = BufWriter::new(File::create(path)?);
209
210        debug!("Writing header...");
211        // Invoke binwrite
212        self.header()
213            .write_le(&mut w)
214            .map_err(|be| Error::while_writing("Header", be))?;
215
216        let link_flags = *self.header().link_flags();
217
218        debug!("Writing StringData...");
219        self.string_data
220            .write_le_args(&mut w, (link_flags, self.encoding))
221            .map_err(|be| Error::while_writing("StringData", be))?;
222
223        // if link_flags.contains(LinkFlags::HAS_LINK_TARGET_ID_LIST) {
224        //     if let None = self.linktarget_id_list {
225        //         error!("LinkTargetIDList not specified but expected!")
226        //     }
227        //     debug!("A LinkTargetIDList is marked as present. Writing.");
228        //     let mut data: Vec<u8> = self.linktarget_id_list.clone().unwrap().into();
229        //     w.write_all(&mut data)?;
230        // }
231
232        // if link_flags.contains(LinkFlags::HAS_LINK_INFO) {
233        //     if let None = self.link_info {
234        //         error!("LinkInfo not specified but expected!")
235        //     }
236        //     debug!("LinkInfo is marked as present. Writing.");
237        //     let mut data: Vec<u8> = self.link_info.clone().unwrap().into();
238        //     w.write_all(&mut data)?;
239        // }
240
241        // if link_flags.contains(LinkFlags::HAS_NAME) {
242        //     if self.name_string == None {
243        //         error!("Name not specified but expected!")
244        //     }
245        //     debug!("Name is marked as present. Writing.");
246        //     w.write_all(&stringdata::to_data(
247        //         self.name_string.as_ref().unwrap(),
248        //         link_flags,
249        //     ))?;
250        // }
251
252        // if link_flags.contains(LinkFlags::HAS_RELATIVE_PATH) {
253        //     if self.relative_path == None {
254        //         error!("Relative path not specified but expected!")
255        //     }
256        //     debug!("Relative path is marked as present. Writing.");
257        //     w.write_all(&stringdata::to_data(
258        //         self.relative_path.as_ref().unwrap(),
259        //         link_flags,
260        //     ))?;
261        // }
262
263        // if link_flags.contains(LinkFlags::HAS_WORKING_DIR) {
264        //     if self.working_dir == None {
265        //         error!("Working Directory not specified but expected!")
266        //     }
267        //     debug!("Working dir is marked as present. Writing.");
268        //     w.write_all(&stringdata::to_data(
269        //         self.working_dir.as_ref().unwrap(),
270        //         link_flags,
271        //     ))?;
272        // }
273
274        // if link_flags.contains(LinkFlags::HAS_ARGUMENTS) {
275        //     if self.icon_location == None {
276        //         error!("Arguments not specified but expected!")
277        //     }
278        //     debug!("Arguments are marked as present. Writing.");
279        //     w.write_all(&stringdata::to_data(
280        //         self.command_line_arguments.as_ref().unwrap(),
281        //         link_flags,
282        //     ))?;
283        // }
284
285        // if link_flags.contains(LinkFlags::HAS_ICON_LOCATION) {
286        //     if self.icon_location == None {
287        //         error!("Icon Location not specified but expected!")
288        //     }
289        //     debug!("Icon Location is marked as present. Writing.");
290        //     w.write_all(&stringdata::to_data(
291        //         self.icon_location.as_ref().unwrap(),
292        //         link_flags,
293        //     ))?;
294        // }
295
296        Ok(())
297    }
298
299    /// Open and parse a shell link
300    ///
301    /// All string which are stored in the `lnk` file are encoded with either
302    /// Unicode (UTF-16LE) of any of the Windows code pages. Which of both is
303    /// being used is specified by the [`LinkFlags::IS_UNICODE`] flag. Microsoft
304    /// documents this as follows:
305    ///
306    /// > If this bit is set, the StringData section contains Unicode-encoded
307    /// > strings; otherwise, it contains strings that are encoded using the
308    /// > system default code page.
309    /// >
310    /// > (<https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-shllink/ae350202-3ba9-4790-9e9e-98935f4ee5af>)
311    ///
312    /// The system default code page is stored in
313    /// `HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Nls\CodePage\ACP`
314    ///
315    /// Because we do not know what the system default code page was, you must
316    /// specify this using the `encoding` parameter (see below). If you you do
317    /// not know the system default code page either, you're lost. There is no
318    /// way to correctly guess the used code page from the data in the `lnk`
319    /// file.
320    ///
321    /// * `path` - path of the `lnk` file to be analyzed
322    /// * `encoding` - character encoding to be used if the `lnk` file is not
323    ///   Unicode encoded
324    pub fn open<P: AsRef<std::path::Path>>(
325        path: P,
326        encoding: crate::strings::Encoding,
327    ) -> Result<Self, Error> {
328        debug!("Opening {:?}", path.as_ref());
329        let mut reader = BufReader::new(File::open(path)?);
330        trace!("Reading file.");
331
332        let shell_link_header: ShellLinkHeader = reader
333            .read_le()
334            .map_err(|be| Error::while_parsing("ShellLinkHeader", be))?;
335        debug!("Shell header: {:#?}", shell_link_header);
336
337        let mut linktarget_id_list = None;
338        let link_flags = *shell_link_header.link_flags();
339        if link_flags.contains(LinkFlags::HAS_LINK_TARGET_ID_LIST) {
340            debug!(
341                "A LinkTargetIDList is marked as present. Parsing now at position 0x{:0x}",
342                reader.stream_position()?
343            );
344            let list: LinkTargetIdList = reader
345                .read_le()
346                .map_err(|be| Error::while_parsing("LinkTargetIdList", be))?;
347            debug!("LinkTargetIDList: {:#?}", list);
348            linktarget_id_list = Some(list);
349        }
350
351        let mut link_info = None;
352        if link_flags.contains(LinkFlags::HAS_LINK_INFO) {
353            let link_info_offset = reader.stream_position().unwrap();
354            debug!(
355                "LinkInfo is marked as present. Parsing now at position 0x{:0x}",
356                reader.stream_position().unwrap()
357            );
358            let info: LinkInfo = reader
359                .read_le_args((encoding,))
360                .map_err(|be| Error::while_parsing("LinkInfo", be))?;
361            debug!("{:#?}", info);
362            debug_assert_eq!(
363                reader.stream_position().unwrap(),
364                link_info_offset + u64::from(*(info.link_info_size()))
365            );
366            link_info = Some(info);
367        }
368
369        debug!(
370            "reading StringData at 0x{:08x}",
371            reader.stream_position().unwrap()
372        );
373        let string_data: StringData = reader
374            .read_le_args((link_flags, encoding))
375            .map_err(|be| Error::while_parsing("StringData", be))?;
376        debug!("{:#?}", string_data);
377
378        debug!(
379            "reading ExtraData at 0x{:08x}",
380            reader.stream_position().unwrap()
381        );
382        let extra_data: ExtraData = reader
383            .read_le_args((encoding,))
384            .map_err(|be| Error::while_parsing("ExtraData", be))?;
385
386        let encoding = if shell_link_header
387            .link_flags()
388            .contains(LinkFlags::IS_UNICODE)
389        {
390            encoding_rs::UTF_16LE
391        } else {
392            encoding
393        };
394
395        Ok(Self {
396            header: shell_link_header,
397            linktarget_id_list,
398            link_info,
399            string_data,
400            extra_data,
401            encoding,
402        })
403    }
404
405    /// returns the full path of the link target. This information
406    /// is constructed completely from the LINK_INFO structure. So,
407    /// if the lnk file does not contain such a structure, the result
408    /// of this method will be `None`
409    pub fn link_target(&self) -> Option<String> {
410        if let Some(info) = self.link_info().as_ref() {
411            let mut base_path = if info
412                .link_info_flags()
413                .has_common_network_relative_link_and_path_suffix()
414            {
415                info.common_network_relative_link()
416                    .as_ref()
417                    .expect("missing common network relative link")
418                    .name()
419            } else {
420                info.local_base_path_unicode()
421                    .as_ref()
422                    .map(|s| &s[..])
423                    .or(info.local_base_path())
424                    .expect("missing local base path")
425                    .to_string()
426            };
427
428            let common_path = info
429                .common_path_suffix_unicode()
430                .as_ref()
431                .map(|s| &s[..])
432                .unwrap_or(info.common_path_suffix());
433
434            // join base_path and common_path;
435            // make sure they're divided by exactly one '\' character.
436            // if common_path is empty, there's nothing to join.
437            if !common_path.is_empty() {
438                if !base_path.ends_with('\\') {
439                    base_path.push('\\');
440                }
441                base_path.push_str(common_path);
442            }
443            Some(base_path)
444        } else {
445            None
446        }
447    }
448
449    /// Set the shell link's name
450    pub fn set_name(&mut self, name: Option<String>) {
451        self.header_mut()
452            .update_link_flags(LinkFlags::HAS_NAME, name.is_some());
453        self.string_data_mut().set_name_string(name);
454    }
455
456    /// Set the shell link's relative path
457    pub fn set_relative_path(&mut self, relative_path: Option<String>) {
458        self.header_mut()
459            .update_link_flags(LinkFlags::HAS_RELATIVE_PATH, relative_path.is_some());
460        self.string_data_mut().set_relative_path(relative_path);
461    }
462
463    /// Set the shell link's working directory
464    pub fn set_working_dir(&mut self, working_dir: Option<String>) {
465        self.header_mut()
466            .update_link_flags(LinkFlags::HAS_WORKING_DIR, working_dir.is_some());
467        self.string_data_mut().set_working_dir(working_dir);
468    }
469
470    /// Set the shell link's arguments
471    pub fn set_arguments(&mut self, arguments: Option<String>) {
472        self.header_mut()
473            .update_link_flags(LinkFlags::HAS_ARGUMENTS, arguments.is_some());
474        self.string_data_mut().set_command_line_arguments(arguments);
475    }
476
477    /// Set the shell link's icon location
478    pub fn set_icon_location(&mut self, icon_location: Option<String>) {
479        self.header_mut()
480            .update_link_flags(LinkFlags::HAS_ICON_LOCATION, icon_location.is_some());
481        self.string_data_mut().set_icon_location(icon_location);
482    }
483}