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}