├── .editorconfig ├── .github └── workflows │ ├── check-version-bumped.yaml │ ├── checks.yaml │ └── publish.yaml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── create_lnk.rs └── read_lnk.rs ├── fuzz.sh ├── fuzz ├── .gitignore ├── Cargo.toml └── fuzz_targets │ └── shell_link_header.rs ├── np.lnk ├── src ├── bin │ └── lnk2json │ │ └── main.rs ├── binread_flags.rs ├── current_offset.rs ├── error.rs ├── extradata.rs ├── extradata │ ├── console_data.rs │ ├── console_fe_data.rs │ ├── darwin_data.rs │ ├── environment_variable_data.rs │ ├── icon_environment_data.rs │ ├── known_folder_data.rs │ ├── property_store_data.rs │ ├── shell_item_identifiers.rs │ ├── shim_data.rs │ ├── special_folder_data.rs │ ├── tracker_data.rs │ └── vista_and_above_id_list_data.rs ├── generic_types │ ├── filetime.rs │ ├── guid.rs │ ├── idlist.rs │ └── mod.rs ├── header.rs ├── header │ ├── file_attributes_flags.rs │ ├── hotkey_flags.rs │ └── link_flags.rs ├── itemid.rs ├── lib.rs ├── linkinfo.rs ├── linktarget.rs ├── stringdata.rs └── strings │ ├── fixed_size_string.rs │ ├── mod.rs │ ├── null_terminated_string.rs │ ├── sized_string.rs │ └── string_encoding.rs └── tests ├── create-read-blank.rs ├── data ├── Hearthstone.lnk ├── Windows PowerShell.lnk ├── blank.txt ├── hotkey-no-problem.lnk ├── hotkey-problem.lnk ├── iron-heart.exe - Shortcut.lnk ├── iron-heart.exe - non-latin Shortcut.lnk └── test.lnk ├── hotkey-key-problem.rs ├── issuecomment-2560550817-failing-shortcut.rs └── test.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 4 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | [*.yaml] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.github/workflows/check-version-bumped.yaml: -------------------------------------------------------------------------------- 1 | name: Check version is suitable for merge to upstream 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | - next 8 | 9 | jobs: 10 | check-version: 11 | name: Check version 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout this PR 16 | uses: actions/checkout@v4 17 | - name: Determine Cargo version of this PR 18 | id: version-pr 19 | run: | 20 | export CARGO_PKG_VERSION=$(awk -F '["=]' '/^\[(workspace.)?package\]/{p=1} p && /^version[[:space:]]*=/ {gsub(/"/, "", $3); print $3; p=0}' Cargo.toml) 21 | export CARGO_PKG_PRERELEASE=$([[ $CARGO_PKG_VERSION =~ -[0-9A-Za-z]+ ]] && echo "true" || echo "false") 22 | echo "CARGO_PKG_VERSION=${CARGO_PKG_VERSION}" >> $GITHUB_OUTPUT 23 | echo "CARGO_PKG_PRERELEASE=${CARGO_PKG_PRERELEASE}" >> $GITHUB_OUTPUT 24 | 25 | - name: Checkout ${{ github.base_ref }} 26 | uses: actions/checkout@v4 27 | with: 28 | ref: ${{ github.base_ref }} 29 | - name: Determine Cargo version of ${{ github.base_ref }} 30 | id: version-upstream 31 | run: | 32 | export CARGO_PKG_VERSION=$(awk -F '["=]' '/^\[(workspace.)?package\]/{p=1} p && /^version[[:space:]]*=/ {gsub(/"/, "", $3); print $3; p=0}' Cargo.toml) 33 | export CARGO_PKG_PRERELEASE=$([[ $CARGO_PKG_VERSION =~ -[0-9A-Za-z]+ ]] && echo "true" || echo "false") 34 | echo "CARGO_PKG_VERSION=${CARGO_PKG_VERSION}" >> $GITHUB_OUTPUT 35 | echo "CARGO_PKG_PRERELEASE=${CARGO_PKG_PRERELEASE}" >> $GITHUB_OUTPUT 36 | 37 | - name: Assert versions are different 38 | run: go run github.com/davidrjonas/semver-cli@latest greater ${{ steps.version-pr.outputs.CARGO_PKG_VERSION }} ${{ steps.version-upstream.outputs.CARGO_PKG_VERSION }} 39 | -------------------------------------------------------------------------------- /.github/workflows/checks.yaml: -------------------------------------------------------------------------------- 1 | name: Rust Checks 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | check: 8 | name: Check ${{ matrix.checks.name }} 9 | runs-on: ubuntu-latest 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | checks: 14 | - name: Code 15 | command: check 16 | - name: Formatting 17 | component: rustfmt 18 | command: fmt --check 19 | - name: Tests 20 | command: test 21 | - name: Clippy Suggestions 22 | component: clippy 23 | command: clippy --all-features 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | 29 | - name: Install latest Rust toolchain 30 | uses: dtolnay/rust-toolchain@stable 31 | with: 32 | components: ${{ matrix.checks.component }} 33 | 34 | - name: Checking ${{ matrix.checks.name }} 35 | run: cargo ${{ matrix.checks.command }} 36 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Build and Publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | publish: 10 | name: Publish 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v1 16 | 17 | - name: Install latest rust toolchain 18 | uses: actions-rs/toolchain@v1 19 | with: 20 | toolchain: stable 21 | default: true 22 | override: true 23 | 24 | - name: Build and Test 25 | run: | 26 | cargo build --release 27 | cargo test 28 | 29 | - name: Sign in to crates.io 30 | run: cargo login ${{ secrets.CARGO_TOKEN }} 31 | 32 | - name: Publish 33 | run: cargo publish 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | **/*.rs.bk 3 | Cargo.lock 4 | /temp.lnk 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lnk" 3 | version = "0.6.1" 4 | authors = [ 5 | "Lily Hopkins ", 6 | "Simon Buchan ", 7 | "Jan Starke ", 8 | ] 9 | edition = "2021" 10 | description = "A Rust library to parse and write Windows shortcut files (.lnk)" 11 | license = "MIT" 12 | repository = "https://github.com/lilopkins/lnk-rs" 13 | 14 | [features] 15 | default = ["serde"] 16 | binwrite = [ "stability" ] 17 | unstable-save = [] 18 | serde = ["dep:serde", "dep:serde_json", "bitflags/serde"] 19 | lnk2json = ["serde", "dep:clap", "dep:simplelog", "dep:clap-verbosity-flag", "dep:clio", "dep:anyhow"] 20 | 21 | [[bin]] 22 | name = "lnk2json" 23 | path = "src/bin/lnk2json/main.rs" 24 | required-features = ["lnk2json"] 25 | 26 | [[example]] 27 | name = "create_lnk" 28 | path = "examples/create_lnk.rs" 29 | required-features = ["binwrite"] 30 | 31 | [[test]] 32 | name = "create-read-blank" 33 | path = "tests/create-read-blank.rs" 34 | required-features = ["binwrite"] 35 | 36 | [dependencies] 37 | log = "0.4.11" 38 | bitflags = "2.9" 39 | chrono = "0.4.23" 40 | binrw = "0.14" 41 | getset = "0.1" 42 | thiserror = "2.0" 43 | encoding_rs = "0.8" 44 | uuid = "1.7" 45 | winstructs = "0.3" 46 | substring = "1.4" 47 | 48 | serde = { version = "1.0", features = ["derive"], optional = true } 49 | serde_json = { version = "1", optional = true } 50 | 51 | clap = { version = "4", features = ["derive", "wrap_help", "cargo"], optional = true } 52 | simplelog = { version = "0.12", optional = true } 53 | clap-verbosity-flag = { version = "3.0.2", optional = true } 54 | clio = { version = "0.3", features = ["clap-parse"], optional = true } 55 | anyhow = { version = "1.0", optional = true } 56 | 57 | stability = {version = "0.2.1", optional = true } 58 | num-traits = "0.2.19" 59 | num-derive = "0.4.2" 60 | 61 | [dev-dependencies] 62 | pretty_env_logger = "0.5.0" 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Lily Hopkins 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shell Link parser and writer for Rust 2 | 3 | Works on any OS - although only really useful in Windows, this library can parse and write 4 | .lnk files, a shell link, that can be understood by Windows. 5 | 6 | To get started, see the [docs.rs documentation](https://docs.rs/lnk/). -------------------------------------------------------------------------------- /examples/create_lnk.rs: -------------------------------------------------------------------------------- 1 | use std::io::Result; 2 | use std::path::Path; 3 | 4 | fn main() -> Result<()> { 5 | pretty_env_logger::init(); 6 | 7 | let shortcut = lnk::ShellLink::new_simple(Path::new(r"C:\Windows\System32\notepad.exe"))?; 8 | shortcut.save("np.lnk").expect("Failed to save shortcut!"); 9 | Ok(()) 10 | } 11 | -------------------------------------------------------------------------------- /examples/read_lnk.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use lnk::encoding::WINDOWS_1252; 4 | 5 | fn main() { 6 | pretty_env_logger::init(); 7 | 8 | let args: Vec = env::args().collect(); 9 | 10 | if args.len() == 1 { 11 | eprintln!("You must specify some file(s) to read!"); 12 | } 13 | 14 | for arg in &args[1..] { 15 | println!("{}: ", arg); 16 | let shortcut = lnk::ShellLink::open(arg, WINDOWS_1252).unwrap(); 17 | println!("{:#?}", shortcut); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /fuzz.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ##### Preparation: 4 | # 5 | # rustup install nightly 6 | # cargo install cargo-fuzz 7 | 8 | RUST_BACKTRACE=1 cargo +nightly fuzz run shell_link_header 9 | -------------------------------------------------------------------------------- /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | corpus 3 | artifacts 4 | coverage 5 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lnk-fuzz" 3 | version = "0.0.0" 4 | publish = false 5 | edition = "2021" 6 | 7 | [package.metadata] 8 | cargo-fuzz = true 9 | 10 | [dependencies] 11 | libfuzzer-sys = "0.4" 12 | binread = "2" 13 | binrw = "0.14" 14 | 15 | [dependencies.lnk] 16 | path = ".." 17 | 18 | [[bin]] 19 | name = "shell_link_header" 20 | path = "fuzz_targets/shell_link_header.rs" 21 | test = false 22 | doc = false 23 | bench = false 24 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/shell_link_header.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | use libfuzzer_sys::fuzz_target; 4 | use lnk::ShellLinkHeader; 5 | use std::io::Cursor; 6 | use binrw::BinReaderExt; 7 | 8 | fuzz_target!(|data: &[u8]| { 9 | let mut cursor = Cursor::new(data); 10 | match cursor.read_le::() { 11 | Err(_) => (), 12 | Ok(header) => println!("fuzzer found as valid header of size {}", header.header_size()) 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /np.lnk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilopkins/lnk-rs/7087c251f026b8f9a9b88a8363eb06cb67d230d5/np.lnk -------------------------------------------------------------------------------- /src/bin/lnk2json/main.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, ValueHint}; 2 | use clio::Input; 3 | use encoding_rs::WINDOWS_1252; 4 | use lnk::ShellLink; 5 | use simplelog::{ColorChoice, Config, TermLogger, TerminalMode}; 6 | 7 | #[derive(Parser)] 8 | #[clap(name="lnk2json", author, version, long_about = None)] 9 | struct Cli { 10 | #[clap(value_hint=ValueHint::FilePath, help="path to lnk file")] 11 | pub(crate) input_file: Input, 12 | 13 | /// pretty print JSON output 14 | #[clap(short('P'), long("pretty"))] 15 | pub(crate) pretty: bool, 16 | 17 | #[clap(flatten)] 18 | pub(crate) verbose: clap_verbosity_flag::Verbosity, 19 | } 20 | 21 | fn main() -> anyhow::Result<()> { 22 | let cli = Cli::parse(); 23 | let _ = TermLogger::init( 24 | cli.verbose.log_level_filter(), 25 | Config::default(), 26 | TerminalMode::Stderr, 27 | ColorChoice::Auto, 28 | ); 29 | 30 | if !cli.input_file.path().exists() { 31 | anyhow::bail!("the file you specified does not exist"); 32 | } 33 | if !cli.input_file.path().is_file() { 34 | anyhow::bail!("you did not specify a file"); 35 | } 36 | 37 | let shell_link = ShellLink::open(cli.input_file.path().path(), WINDOWS_1252)?; 38 | 39 | if cli.pretty { 40 | println!("{}", serde_json::to_string_pretty(&shell_link)?); 41 | } else { 42 | println!("{}", serde_json::to_string(&shell_link)?); 43 | } 44 | Ok(()) 45 | } 46 | -------------------------------------------------------------------------------- /src/binread_flags.rs: -------------------------------------------------------------------------------- 1 | macro_rules! binread_flags { 2 | ($type: ty, $repr:ty) => { 3 | impl binrw::BinRead for $type { 4 | type Args<'a> = (); 5 | 6 | fn read_options( 7 | reader: &mut R, 8 | endian: binrw::Endian, 9 | _args: Self::Args<'_>, 10 | ) -> binrw::BinResult { 11 | use binrw::BinReaderExt; 12 | let raw: $repr = match endian { 13 | binrw::Endian::Big => reader.read_be()?, 14 | binrw::Endian::Little => reader.read_le()?, 15 | }; 16 | 17 | match Self::from_bits(raw) { 18 | Some(res) => Ok(res), 19 | None => Err(binrw::Error::AssertFail { 20 | pos: reader.stream_position()?, 21 | message: format!("unable to convert '0x{raw:x}' to {}", stringify!($type)), 22 | }), 23 | } 24 | } 25 | } 26 | 27 | impl binrw::BinWrite for $type { 28 | type Args<'a> = (); 29 | 30 | fn write_options( 31 | &self, 32 | writer: &mut W, 33 | endian: binrw::Endian, 34 | _args: Self::Args<'_>, 35 | ) -> binrw::BinResult<()> { 36 | match endian { 37 | binrw::Endian::Big => self.bits().write_be(writer), 38 | binrw::Endian::Little => self.bits().write_le(writer), 39 | } 40 | } 41 | } 42 | }; 43 | } 44 | 45 | pub(crate) use binread_flags; 46 | -------------------------------------------------------------------------------- /src/current_offset.rs: -------------------------------------------------------------------------------- 1 | use binrw::BinRead; 2 | use log::trace; 3 | 4 | /// implements [`BinRead`] by reading the current cursor position 5 | /// and storing it as `u32` 6 | #[derive(Clone, Debug)] 7 | pub struct CurrentOffset(u32); 8 | 9 | impl BinRead for CurrentOffset { 10 | type Args<'a> = (); 11 | 12 | fn read_options( 13 | reader: &mut R, 14 | _endian: binrw::Endian, 15 | _args: Self::Args<'_>, 16 | ) -> binrw::BinResult { 17 | let pos = reader.stream_position()?; 18 | trace!("read offset at 0x{pos:016x}"); 19 | Ok(Self(pos.try_into().expect("invalid offset"))) 20 | } 21 | } 22 | 23 | impl AsRef for CurrentOffset { 24 | fn as_ref(&self) -> &u32 { 25 | &self.0 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::io::ErrorKind; 2 | 3 | use thiserror::Error; 4 | 5 | /// The error type for shell link parsing errors. 6 | #[derive(Debug, Error)] 7 | #[allow(missing_docs)] 8 | pub enum Error { 9 | #[error("An IO error occurred: {0}")] 10 | IoError(#[from] std::io::Error), 11 | 12 | #[error("The parsed file isn't a shell link.")] 13 | NotAShellLinkError, 14 | 15 | #[error("Unexpected End-of-File while expecting a '{0}' instead")] 16 | UnexpectedEof(&'static str), 17 | 18 | #[error("Error while parsing {0}: {1}")] 19 | BinReadError(&'static str, binrw::Error), 20 | 21 | #[error("Error while writing {0}: {1}")] 22 | BinWriteError(&'static str, binrw::Error), 23 | } 24 | 25 | impl Error { 26 | /// creates an [`Error::BinReadError`] instance which wraps a [`binrw::Error`] 27 | /// together with some context information which describes where the error 28 | /// has occurred. 29 | pub fn while_parsing(context: &'static str, be: binrw::Error) -> Self { 30 | if let binrw::Error::Io(ref why) = be { 31 | if why.kind() == ErrorKind::UnexpectedEof { 32 | return Self::UnexpectedEof(context); 33 | } 34 | } 35 | Self::BinReadError(context, be) 36 | } 37 | 38 | /// creates an [`Error::BinWriteError`] instance which wraps a [`binrw::Error`] 39 | /// together with some context information which describes where the error 40 | /// has occurred. 41 | pub fn while_writing(context: &'static str, be: binrw::Error) -> Self { 42 | Self::BinWriteError(context, be) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/extradata.rs: -------------------------------------------------------------------------------- 1 | use std::io::ErrorKind; 2 | 3 | use binrw::{BinRead, BinReaderExt}; 4 | use encoding_rs::Encoding; 5 | #[allow(unused)] 6 | use log::{debug, error, info, trace, warn}; 7 | 8 | #[cfg(feature = "serde")] 9 | use serde::Serialize; 10 | 11 | use self::{ 12 | console_data::ConsoleDataBlock, console_fe_data::ConsoleFEDataBlock, 13 | darwin_data::DarwinDataBlock, environment_variable_data::EnvironmentVariableDataBlock, 14 | icon_environment_data::IconEnvironmentDataBlock, known_folder_data::KnownFolderDataBlock, 15 | property_store_data::PropertyStoreDataBlock, shell_item_identifiers::ShellItemIdentifiers, 16 | shim_data::ShimDataBlock, special_folder_data::SpecialFolderDataBlock, 17 | tracker_data::TrackerDataBlock, vista_and_above_id_list_data::VistaAndAboveIdListDataBlock, 18 | }; 19 | 20 | /// The ConsoleDataBlock structure specifies the display settings to use 21 | /// when a link target specifies an application that is run in a console 22 | /// window. 23 | pub mod console_data; 24 | 25 | /// The ConsoleFEDataBlock structure specifies the code page to use 26 | /// for displaying text when a link target specifies an application 27 | /// that is run in a console window. 28 | pub mod console_fe_data; 29 | 30 | /// The DarwinDataBlock structure specifies an application identifier 31 | /// that can be used instead of a link target IDList to install an 32 | /// application when a shell link is activated. 33 | pub mod darwin_data; 34 | 35 | /// The EnvironmentVariableDataBlock structure specifies a path to 36 | /// environment variable information when the link target refers to 37 | /// a location that has a corresponding environment variable. 38 | pub mod environment_variable_data; 39 | 40 | /// The IconEnvironmentDataBlock structure specifies the path to an 41 | /// icon. The path is encoded using environment variables, which makes 42 | /// it possible to find the icon across machines where the locations 43 | /// vary but are expressed using environment variables. 44 | pub mod icon_environment_data; 45 | 46 | /// The KnownFolderDataBlock structure specifies the location of a 47 | /// known folder. This data can be used when a link target is a 48 | /// known folder to keep track of the folder so that the link target 49 | /// IDList can be translated when the link is loaded. 50 | pub mod known_folder_data; 51 | 52 | /// A PropertyStoreDataBlock structure specifies a set of properties 53 | /// that can be used by applications to store extra data in the 54 | /// shell link. 55 | pub mod property_store_data; 56 | 57 | /// The ShimDataBlock structure specifies the name of a shim that can 58 | /// be applied when activating a link target. 59 | pub mod shim_data; 60 | 61 | /// The SpecialFolderDataBlock structure specifies the location of a 62 | /// special folder. This data can be used when a link target is a 63 | /// special folder to keep track of the folder, so that the link target 64 | /// IDList can be translated when the link is loaded. 65 | pub mod special_folder_data; 66 | 67 | /// The TrackerDataBlock structure specifies data that can be used to 68 | /// resolve a link target if it is not found in its original location 69 | /// when the link is resolved. This data is passed to the Link 70 | /// Tracking service [MS-DLTW] to find the link target. 71 | pub mod tracker_data; 72 | 73 | /// The VistaAndAboveIDListDataBlock structure specifies an alternate 74 | /// IDList that can be used instead of the LinkTargetIDList structure 75 | /// (section 2.2) on platforms that support it. 76 | pub mod vista_and_above_id_list_data; 77 | 78 | /// ExtraData refers to a set of structures that convey additional information 79 | /// about a link target. These optional structures can be present in an extra 80 | /// data section that is appended to the basic Shell Link Binary File Format. 81 | /// 82 | /// At the moment, ExtraData can only be read, not written to shortcuts 83 | mod shell_item_identifiers; 84 | 85 | #[allow(missing_docs)] 86 | #[derive(Clone, Debug, BinRead)] 87 | #[cfg_attr(feature = "serde", derive(Serialize))] 88 | #[br(import(_block_size: u32, _default_codepage: &'static Encoding))] 89 | pub enum ExtraDataBlock { 90 | #[br(magic = 0xa0000001u32)] 91 | EnvironmentProps(#[br(args(_block_size, _default_codepage))] EnvironmentVariableDataBlock), 92 | #[br(magic = 0xa0000002u32)] 93 | ConsoleProps(#[br(args(_block_size))] ConsoleDataBlock), 94 | #[br(magic = 0xa0000003u32)] 95 | TrackerProps(#[br(args(_block_size, _default_codepage))] TrackerDataBlock), 96 | #[br(magic = 0xa0000004u32)] 97 | ConsoleFeProps(#[br(args(_block_size))] ConsoleFEDataBlock), 98 | #[br(magic = 0xa0000005u32)] 99 | SpecialFolderProps(#[br(args(_block_size))] SpecialFolderDataBlock), 100 | #[br(magic = 0xa0000006u32)] 101 | DarwinProps(#[br(args(_block_size, _default_codepage))] DarwinDataBlock), 102 | #[br(magic = 0xa0000007u32)] 103 | IconEnvironmentProps(#[br(args(_block_size, _default_codepage))] IconEnvironmentDataBlock), 104 | #[br(magic = 0xa0000008u32)] 105 | ShimProps(#[br(args(_block_size))] ShimDataBlock), 106 | #[br(magic = 0xa0000009u32)] 107 | PropertyStoreProps(#[br(args(_block_size))] PropertyStoreDataBlock), 108 | #[br(magic = 0xa000000au32)] 109 | VistaAndAboveIdListProps(#[br(args(_block_size))] VistaAndAboveIdListDataBlock), 110 | #[br(magic = 0xa000000bu32)] 111 | KnownFolderProps(#[br(args(_block_size))] KnownFolderDataBlock), 112 | #[br(magic = 0xa000000cu32)] 113 | ShellItemIdentifiers(#[br(args(_block_size))] ShellItemIdentifiers), 114 | } 115 | 116 | #[derive(Default, Debug)] 117 | #[allow(missing_docs, unused)] 118 | #[cfg_attr(feature = "serde", derive(Serialize))] 119 | pub struct ExtraData { 120 | blocks: Vec, 121 | } 122 | 123 | impl BinRead for ExtraData { 124 | type Args<'a> = (&'static Encoding,); 125 | 126 | fn read_options( 127 | reader: &mut R, 128 | _endian: binrw::Endian, 129 | args: Self::Args<'_>, 130 | ) -> binrw::BinResult { 131 | let mut blocks = Vec::new(); 132 | loop { 133 | let block_size: u32 = match reader.read_le() { 134 | Ok(block_size) => block_size, 135 | Err(binrw::Error::Io(why)) => { 136 | if why.kind() == ErrorKind::UnexpectedEof { 137 | break; 138 | } else { 139 | return Err(binrw::Error::Io(why)); 140 | } 141 | } 142 | Err(why) => return Err(why), 143 | }; 144 | 145 | if block_size == 0 { 146 | break; 147 | } else { 148 | let block: ExtraDataBlock = reader.read_le_args((block_size, args.0))?; 149 | blocks.push(block); 150 | } 151 | } 152 | Ok(Self { blocks }) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/extradata/console_data.rs: -------------------------------------------------------------------------------- 1 | use binrw::BinRead; 2 | use bitflags::bitflags; 3 | use encoding_rs::UTF_16LE; 4 | use getset::Getters; 5 | 6 | use crate::{binread_flags::binread_flags, strings::FixedSizeString}; 7 | 8 | #[cfg(feature = "serde")] 9 | use serde::Serialize; 10 | 11 | bitflags! { 12 | /// A 16-bit, unsigned integer that specifies the fill attributes that 13 | /// control the foreground and background text colors in the console 14 | /// window. The following bit definitions can be combined to specify 16 15 | /// different values each for the foreground and background colors: 16 | #[derive(Clone, Debug, Eq, PartialEq)] 17 | #[cfg_attr(feature = "serde", derive(Serialize))] 18 | pub struct FillAttributeFlags: u16 { 19 | /// The foreground text color contains blue. 20 | const FOREGROUND_BLUE = 0b0000_0000_0000_0001; 21 | /// The foreground text color contains green. 22 | const FOREGROUND_GREEN = 0b0000_0000_0000_0010; 23 | /// The foreground text color contains red. 24 | const FOREGROUND_RED = 0b0000_0000_0000_0100; 25 | /// The foreground text color is intensified. 26 | const FOREGROUND_INTENSITY = 0b0000_0000_0000_1000; 27 | 28 | /// The background text color contains blue. 29 | const BACKGROUND_BLUE = 0b0000_0000_0001_0000; 30 | /// The background text color contains green. 31 | const BACKGROUND_GREEN = 0b0000_0000_0010_0000; 32 | /// The background text color contains red. 33 | const BACKGROUND_RED = 0b0000_0000_0100_0000; 34 | /// The background text color is intensified. 35 | const BACKGROUND_INTENSITY = 0b0000_0000_1000_0000; 36 | } 37 | } 38 | 39 | binread_flags!(FillAttributeFlags, u16); 40 | 41 | bitflags! { 42 | /// A 32-bit, unsigned integer that specifies the family of the font 43 | /// used in the console window. This value MUST be comprised of a font 44 | /// family and an optional font pitch. 45 | #[derive(Clone, Debug, Eq, PartialEq)] 46 | #[cfg_attr(feature = "serde", derive(Serialize))] 47 | pub struct FontFamilyFlags: u32 { 48 | /// The font family is unknown. 49 | const FF_DONT_CARE = 0x0000; 50 | /// The font is variable-width with serifs; for example, "Times New Roman". 51 | const FF_ROMAN = 0x0010; 52 | /// The font is variable-width without serifs; for example, "Arial". 53 | const FF_SWISS = 0x0020; 54 | /// The font is fixed-width, with or without serifs; for example, "Courier New". 55 | const FF_MODERN = 0x0030; 56 | /// The font is designed to look like handwriting; for example, "Cursive". 57 | const FF_SCRIPT = 0x0040; 58 | /// The font is a novelty font; for example, "Old English". 59 | const FF_DECORATIVE = 0x0050; 60 | 61 | /// The font is a fixed-pitch font. 62 | const TMPF_FIXED_PITCH = 0x0001; 63 | /// The font is a vector font. 64 | const TMPF_VECTOR = 0x0002; 65 | /// The font is a true-type font. 66 | const TMPF_TRUETYPE = 0x0004; 67 | /// The font is specific to the device. 68 | const TMPF_DEVICE = 0x0008; 69 | } 70 | } 71 | 72 | binread_flags!(FontFamilyFlags, u32); 73 | 74 | /// The ConsoleDataBlock structure specifies the display settings to use 75 | /// when a link target specifies an application that is run in a console 76 | /// window. 77 | #[derive(Clone, Debug, Getters, BinRead)] 78 | #[cfg_attr(feature = "serde", derive(Serialize))] 79 | #[br(import(block_size: u32), pre_assert(block_size == 0x0000_00CC))] 80 | #[get(get = "pub")] 81 | #[allow(unused)] 82 | pub struct ConsoleDataBlock { 83 | /// A 16-bit, unsigned integer that specifies the fill attributes that 84 | /// control the foreground and background text colors in the console 85 | /// window. The following bit definitions can be combined to specify 16 86 | /// different values each for the foreground and background colors: 87 | fill_attributes: FillAttributeFlags, 88 | /// A 16-bit, unsigned integer that specifies the fill attributes that 89 | /// control the foreground and background text color in the console 90 | /// window popup. The values are the same as for the FillAttributes 91 | /// field. 92 | popup_fill_attributes: FillAttributeFlags, 93 | /// A 16-bit, signed integer that specifies the horizontal size (X axis), 94 | /// in characters, of the console window buffer. 95 | screen_buffer_size_x: i16, 96 | /// A 16-bit, signed integer that specifies the vertical size (Y axis), 97 | /// in characters, of the console window buffer. 98 | screen_buffer_size_y: i16, 99 | /// A 16-bit, signed integer that specifies the horizontal size (X axis), 100 | /// in characters, of the console window. 101 | window_size_x: i16, 102 | /// A 16-bit, signed integer that specifies the vertical size (Y axis), 103 | /// in characters, of the console window. 104 | window_size_y: i16, 105 | /// A 16-bit, signed integer that specifies the horizontal coordinate (X axis), 106 | /// in pixels, of the console window origin. 107 | window_origin_x: i16, 108 | /// A 16-bit, signed integer that specifies the vertical coordinate (Y axis), 109 | /// in pixels, of the console window origin. 110 | window_origin_y: i16, 111 | 112 | #[getset(skip)] 113 | unused1: u32, 114 | 115 | #[getset(skip)] 116 | unused2: u32, 117 | 118 | /// A 32-bit, unsigned integer that specifies the size, in pixels, of the 119 | /// font used in the console window. The two most significant bytes contain 120 | /// the font height and the two least significant bytes contain the font 121 | /// width. For vector fonts, the width is set to zero. 122 | font_size: u32, 123 | /// A 32-bit, unsigned integer that specifies the family of the font used 124 | /// in the console window. This value MUST be comprised of a font family 125 | /// and an optional font pitch. 126 | font_family: FontFamilyFlags, 127 | /// A 32-bit, unsigned integer that specifies the stroke weight of the font 128 | /// used in the console window. 129 | font_weight: u32, 130 | /// A 32-character Unicode string that specifies the face name of the font 131 | /// used in the console window. 132 | #[br(args(64,UTF_16LE), map=|s:FixedSizeString| s.to_string())] 133 | face_name: String, 134 | /// A 32-bit, unsigned integer that specifies the size of the cursor, in 135 | /// pixels, used in the console window. 136 | cursor_size: u32, 137 | /// A 32-bit, unsigned integer that specifies whether to open the console 138 | /// window in full-screen mode. 139 | #[br(map=|b:u32| b != 0x00000000)] 140 | full_screen: bool, 141 | /// A 32-bit, unsigned integer that specifies whether to open the console 142 | /// window in QuikEdit mode. In QuickEdit mode, the mouse can be used to 143 | /// cut, copy, and paste text in the console window. 144 | #[br(map=|b:u32| b != 0x00000000)] 145 | quick_edit: bool, 146 | /// A 32-bit, unsigned integer that specifies insert mode in the console 147 | /// window. 148 | #[br(map=|b:u32| b != 0x00000000)] 149 | insert_mode: bool, 150 | /// A 32-bit, unsigned integer that specifies auto-position mode of the 151 | /// console window. 152 | #[br(map=|b:u32| b != 0x00000000)] 153 | auto_position: bool, 154 | /// A 32-bit, unsigned integer that specifies the size, in characters, of 155 | /// the buffer that is used to store a history of user input into the 156 | /// console window. 157 | history_buffer_size: u32, 158 | /// A 32-bit, unsigned integer that specifies the number of history 159 | /// buffers to use. 160 | number_of_history_buffers: u32, 161 | /// A 32-bit, unsigned integer that specifies whether to remove duplicates 162 | /// in the history buffer. 163 | #[br(map=|b:u32| b != 0x00000000)] 164 | history_no_dup: bool, 165 | /// A table of 16 32-bit, unsigned integers specifying the RGB colors that 166 | /// are used for text in the console window. The values of the fill 167 | /// attribute fields FillAttributes and PopupFillAttributes are used as 168 | /// indexes into this table to specify the final foreground and background 169 | /// color for a character. 170 | color_table: [u32; 16], 171 | } 172 | -------------------------------------------------------------------------------- /src/extradata/console_fe_data.rs: -------------------------------------------------------------------------------- 1 | use binrw::BinRead; 2 | use getset::Getters; 3 | 4 | #[cfg(feature = "serde")] 5 | use serde::Serialize; 6 | 7 | /// The ConsoleFEDataBlock structure specifies the code page to use 8 | /// for displaying text when a link target specifies an application 9 | /// that is run in a console window. 10 | #[derive(Clone, Copy, Debug, BinRead, Getters)] 11 | #[cfg_attr(feature = "serde", derive(Serialize))] 12 | #[br(import(block_size: u32), pre_assert(block_size == 0x0000_0000C))] 13 | #[get(get = "pub")] 14 | #[allow(unused)] 15 | pub struct ConsoleFEDataBlock { 16 | /// A 32-bit, unsigned integer that specifies a code page language 17 | /// code identifier. For details concerning the structure and 18 | /// meaning of language code identifiers, see [MS-LCID]. 19 | code_page: u32, 20 | } 21 | -------------------------------------------------------------------------------- /src/extradata/darwin_data.rs: -------------------------------------------------------------------------------- 1 | use binrw::BinRead; 2 | use encoding_rs::{Encoding, UTF_16LE}; 3 | use getset::Getters; 4 | 5 | #[cfg(feature = "serde")] 6 | use serde::Serialize; 7 | 8 | use crate::strings::FixedSizeString; 9 | 10 | /// The DarwinDataBlock structure specifies an application identifier 11 | /// that can be used instead of a link target IDList to install an 12 | /// application when a shell link is activated. 13 | #[derive(Clone, Debug, BinRead, Getters)] 14 | #[cfg_attr(feature = "serde", derive(Serialize))] 15 | #[br(import(block_size: u32, default_codepage: &'static Encoding), pre_assert(block_size == 0x0000_00314))] 16 | #[get(get = "pub")] 17 | #[allow(unused)] 18 | pub struct DarwinDataBlock { 19 | /// A NULL–terminated string, defined by the system default code 20 | /// page, which specifies an application identifier. This field 21 | /// SHOULD be ignored. 22 | #[br(args(260, default_codepage), map=|s:FixedSizeString| s.to_string())] 23 | darwin_data_ansi: String, 24 | 25 | /// An optional, NULL–terminated, Unicode string that specifies 26 | /// an application identifier. 27 | #[br(args(520, UTF_16LE), map=|s: FixedSizeString| if s.is_empty() {None} else {Some(s.to_string())})] 28 | darwin_data_unicode: Option, 29 | } 30 | -------------------------------------------------------------------------------- /src/extradata/environment_variable_data.rs: -------------------------------------------------------------------------------- 1 | use binrw::BinRead; 2 | use encoding_rs::{Encoding, UTF_16LE}; 3 | use getset::Getters; 4 | 5 | #[cfg(feature = "serde")] 6 | use serde::Serialize; 7 | 8 | use crate::strings::FixedSizeString; 9 | 10 | /// The EnvironmentVariableDataBlock structure specifies a path to 11 | /// environment variable information when the link target refers to 12 | /// a location that has a corresponding environment variable. 13 | #[derive(Clone, Debug, BinRead, Getters)] 14 | #[cfg_attr(feature = "serde", derive(Serialize))] 15 | #[br(import(block_size: u32, default_codepage: &'static Encoding), pre_assert(block_size == 0x0000_0314))] 16 | #[get(get = "pub")] 17 | #[allow(unused)] 18 | pub struct EnvironmentVariableDataBlock { 19 | /// A NULL-terminated string, defined by the system default code 20 | /// page, which specifies a path to environment variable information. 21 | #[br(args(260, default_codepage), map=|s:FixedSizeString| s.to_string())] 22 | target_ansi: String, 23 | /// An optional, NULL-terminated, Unicode string that specifies a path 24 | /// to environment variable information. 25 | #[br(args(520, UTF_16LE), map=|s: FixedSizeString| if s.is_empty() {None} else {Some(s.to_string())})] 26 | target_unicode: Option, 27 | } 28 | -------------------------------------------------------------------------------- /src/extradata/icon_environment_data.rs: -------------------------------------------------------------------------------- 1 | use binrw::BinRead; 2 | use encoding_rs::{Encoding, UTF_16LE}; 3 | use getset::Getters; 4 | 5 | #[cfg(feature = "serde")] 6 | use serde::Serialize; 7 | 8 | use crate::strings::FixedSizeString; 9 | 10 | /// The IconEnvironmentDataBlock structure specifies the path to an 11 | /// icon. The path is encoded using environment variables, which makes 12 | /// it possible to find the icon across machines where the locations 13 | /// vary but are expressed using environment variables. 14 | #[derive(Clone, Debug, BinRead, Getters)] 15 | #[cfg_attr(feature = "serde", derive(Serialize))] 16 | #[br(import(block_size: u32, default_codepage: &'static Encoding), pre_assert(block_size == 0x0000_00314))] 17 | #[get(get = "pub")] 18 | #[allow(unused)] 19 | pub struct IconEnvironmentDataBlock { 20 | /// A NULL-terminated string, defined by the system default code 21 | /// page, which specifies a path that is constructed with 22 | /// environment variables. 23 | #[br(args(260, default_codepage), map=|s:FixedSizeString| s.to_string())] 24 | target_ansi: String, 25 | /// An optional, NULL-terminated, Unicode string that specifies a 26 | /// path that is constructed with environment variables. 27 | #[br(args(520, UTF_16LE), map=|s: FixedSizeString| if s.is_empty() {None} else {Some(s.to_string())})] 28 | target_unicode: Option, 29 | } 30 | -------------------------------------------------------------------------------- /src/extradata/known_folder_data.rs: -------------------------------------------------------------------------------- 1 | use binrw::BinRead; 2 | use getset::Getters; 3 | 4 | #[cfg(feature = "serde")] 5 | use serde::Serialize; 6 | 7 | use crate::Guid; 8 | 9 | /// The KnownFolderDataBlock structure specifies the location of a 10 | /// known folder. This data can be used when a link target is a 11 | /// known folder to keep track of the folder so that the link target 12 | /// IDList can be translated when the link is loaded. 13 | #[derive(Clone, Copy, Debug, BinRead, Getters)] 14 | #[cfg_attr(feature = "serde", derive(Serialize))] 15 | #[br(import(block_size: u32), pre_assert(block_size == 0x0000_0001C))] 16 | #[get(get = "pub")] 17 | #[allow(unused)] 18 | pub struct KnownFolderDataBlock { 19 | /// A value in GUID packet representation ([MS-DTYP] section 20 | /// 2.3.4.2) that specifies the folder GUID ID. 21 | known_folder_id: Guid, 22 | /// A 32-bit, unsigned integer that specifies the location 23 | /// of the ItemID of the first child segment of the IDList specified 24 | /// by KnownFolderID. This value is the offset, in bytes, into the 25 | /// link target IDList. 26 | offset: u32, 27 | } 28 | -------------------------------------------------------------------------------- /src/extradata/property_store_data.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, mem::size_of}; 2 | 3 | #[cfg(feature = "serde")] 4 | use serde::Serialize; 5 | 6 | use binrw::BinRead; 7 | use getset::Getters; 8 | 9 | /// A PropertyStoreDataBlock structure specifies a set of properties 10 | /// that can be used by applications to store extra data in the 11 | /// shell link. 12 | /// TODO: implement 13 | #[derive(Clone, BinRead, Getters)] 14 | #[cfg_attr(feature = "serde", derive(Serialize))] 15 | #[br(import(block_size: u32), pre_assert(block_size >= 0x0000_000C))] 16 | #[get(get = "pub")] 17 | #[allow(unused)] 18 | pub struct PropertyStoreDataBlock { 19 | /// A serialized property storage structure ([MS-PROPSTORE] section 2.2). 20 | #[br(count=block_size - u32::try_from(2*size_of::()).unwrap())] 21 | property_store: Vec, 22 | } 23 | 24 | impl fmt::Debug for PropertyStoreDataBlock { 25 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 26 | write!( 27 | f, 28 | "PropertyStoreDataBlock {{ property_store: (serialized property storage structure) }}" 29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/extradata/shell_item_identifiers.rs: -------------------------------------------------------------------------------- 1 | use binrw::BinRead; 2 | use getset::Getters; 3 | use serde::Serialize; 4 | 5 | use crate::generic_types::idlist::IdList; 6 | 7 | #[derive(Clone, Debug, BinRead, Getters)] 8 | #[cfg_attr(feature = "serde", derive(Serialize))] 9 | #[br(import(block_size: u32), pre_assert(block_size != 10))] 10 | #[get(get = "pub")] 11 | #[allow(unused)] 12 | /// Contains a list of item identifiers. 13 | /// 14 | pub struct ShellItemIdentifiers { 15 | #[br(args((block_size - 8).try_into().unwrap()))] 16 | items: IdList, 17 | } 18 | -------------------------------------------------------------------------------- /src/extradata/shim_data.rs: -------------------------------------------------------------------------------- 1 | use std::mem::size_of; 2 | 3 | use binrw::BinRead; 4 | use encoding_rs::UTF_16LE; 5 | use getset::Getters; 6 | 7 | #[cfg(feature = "serde")] 8 | use serde::Serialize; 9 | 10 | use crate::strings::FixedSizeString; 11 | 12 | /// The ShimDataBlock structure specifies the name of a shim that can 13 | /// be applied when activating a link target. 14 | #[derive(Clone, Debug, BinRead, Getters)] 15 | #[cfg_attr(feature = "serde", derive(Serialize))] 16 | #[br(import(block_size: u32), pre_assert(block_size >= 0x0000_00088))] 17 | #[get(get = "pub")] 18 | #[allow(unused)] 19 | pub struct ShimDataBlock { 20 | /// A Unicode string that specifies the name of a shim layer to apply 21 | /// to a link target when it is being activated. 22 | #[br(args(usize::try_from(block_size).unwrap() - 2*size_of::(), UTF_16LE), map=|s:FixedSizeString| s.to_string())] 23 | layer_name: String, 24 | } 25 | -------------------------------------------------------------------------------- /src/extradata/special_folder_data.rs: -------------------------------------------------------------------------------- 1 | use binrw::BinRead; 2 | use getset::Getters; 3 | 4 | #[cfg(feature = "serde")] 5 | use serde::Serialize; 6 | 7 | /// The SpecialFolderDataBlock structure specifies the location of a 8 | /// special folder. This data can be used when a link target is a 9 | /// special folder to keep track of the folder, so that the link target 10 | /// IDList can be translated when the link is loaded. 11 | #[derive(Clone, Copy, Debug, BinRead, Getters)] 12 | #[cfg_attr(feature = "serde", derive(Serialize))] 13 | #[br(import(block_size: u32), pre_assert(block_size == 0x0000_00010))] 14 | #[get(get = "pub")] 15 | #[allow(unused)] 16 | pub struct SpecialFolderDataBlock { 17 | /// A 32-bit, unsigned integer that specifies the folder integer ID. 18 | special_folder_id: u32, 19 | /// A 32-bit, unsigned integer that specifies the location of the 20 | /// ItemID of the first child segment of the IDList specified by 21 | /// SpecialFolderID. This value is the offset, in bytes, into the 22 | /// link target IDList. 23 | offset: u32, 24 | } 25 | -------------------------------------------------------------------------------- /src/extradata/tracker_data.rs: -------------------------------------------------------------------------------- 1 | use binrw::BinRead; 2 | use encoding_rs::Encoding; 3 | 4 | #[cfg(feature = "serde")] 5 | use serde::Serialize; 6 | 7 | use crate::{strings::FixedSizeString, Guid}; 8 | 9 | /// The TrackerDataBlock structure specifies data that can be used to 10 | /// resolve a link target if it is not found in its original location 11 | /// when the link is resolved. This data is passed to the Link 12 | /// Tracking service [MS-DLTW] to find the link target. 13 | #[derive(Clone, Debug, BinRead)] 14 | #[cfg_attr(feature = "serde", derive(Serialize))] 15 | #[br(import(block_size: u32, default_codepage: &'static Encoding), pre_assert(block_size == 0x0000_00060))] 16 | #[allow(unused)] 17 | pub struct TrackerDataBlock { 18 | /// A 32-bit, unsigned integer that specifies the size of the rest of the 19 | /// TrackerDataBlock structure, including this Length field. This value 20 | /// MUST be 0x00000058. 21 | #[br(assert(length == 0x00000058))] 22 | length: u32, 23 | 24 | /// A 32-bit, unsigned integer. This value MUST be 0x00000000 25 | #[br(assert(version == 0x00000000))] 26 | version: u32, 27 | 28 | /// A NULL–terminated character string, as defined by the system default 29 | /// code page, which specifies the NetBIOS name of the machine where 30 | /// the link target was last known to reside. 31 | #[br(args(16, default_codepage), map=|s:FixedSizeString| s.to_string())] 32 | machine_id: String, 33 | /// Two values in GUID packet representation ([MS-DTYP] section 2.3.4.2) 34 | /// that are used to find the link target with the Link Tracking service, 35 | /// as described in [MS-DLTW]. 36 | droid: [Guid; 2], 37 | /// Two values in GUID packet representation that are used to find the 38 | /// link target with the Link Tracking service 39 | droid_birth: [Guid; 2], 40 | } 41 | -------------------------------------------------------------------------------- /src/extradata/vista_and_above_id_list_data.rs: -------------------------------------------------------------------------------- 1 | use std::mem::size_of; 2 | 3 | use binrw::BinRead; 4 | use getset::Getters; 5 | 6 | #[cfg(feature = "serde")] 7 | use serde::Serialize; 8 | 9 | use crate::IdList; 10 | 11 | /// The VistaAndAboveIDListDataBlock structure specifies an alternate 12 | /// IDList that can be used instead of the LinkTargetIDList structure 13 | /// (section 2.2) on platforms that support it. 14 | #[derive(Clone, Debug, BinRead, Getters)] 15 | #[cfg_attr(feature = "serde", derive(Serialize))] 16 | #[br(import(block_size: u32), pre_assert(block_size >= 0x0000_0000A))] 17 | #[get(get = "pub")] 18 | #[allow(unused)] 19 | pub struct VistaAndAboveIdListDataBlock { 20 | /// An IDList structure (section 2.2.1). 21 | #[br(args(u16::try_from(block_size).unwrap() - u16::try_from(2*size_of::()).unwrap()))] 22 | id_list: IdList, 23 | } 24 | -------------------------------------------------------------------------------- /src/generic_types/filetime.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use binrw::{BinRead, BinReaderExt, BinWrite}; 4 | use chrono::NaiveDateTime; 5 | 6 | #[cfg(feature = "serde")] 7 | use serde::Serialize; 8 | use winstructs::timestamp::WinTimestamp; 9 | 10 | /// The FILETIME structure is a 64-bit value that represents the number of 11 | /// 100-nanosecond intervals that have elapsed since January 1, 1601, 12 | /// Coordinated Universal Time (UTC). 13 | #[derive(Clone)] 14 | pub struct FileTime(WinTimestamp, u64); 15 | 16 | impl fmt::Debug for FileTime { 17 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 18 | write!(f, "{}", self.datetime()) 19 | } 20 | } 21 | 22 | impl BinRead for FileTime { 23 | type Args<'a> = (); 24 | 25 | fn read_options( 26 | reader: &mut R, 27 | _endian: binrw::Endian, 28 | _args: Self::Args<'_>, 29 | ) -> binrw::BinResult { 30 | let pos = reader.stream_position()?; 31 | let raw: u64 = reader.read_le()?; 32 | 33 | match WinTimestamp::new(&raw.to_le_bytes()) { 34 | Ok(timestamp) => Ok(Self(timestamp, raw)), 35 | Err(why) => Err(binrw::Error::AssertFail { 36 | pos, 37 | message: format!("{why}"), 38 | }), 39 | } 40 | } 41 | } 42 | 43 | impl BinWrite for FileTime { 44 | type Args<'a> = (); 45 | 46 | fn write_options( 47 | &self, 48 | writer: &mut W, 49 | _endian: binrw::Endian, 50 | _args: Self::Args<'_>, 51 | ) -> binrw::BinResult<()> { 52 | self.1.write_le(writer) 53 | } 54 | } 55 | 56 | #[cfg(feature = "serde")] 57 | impl Serialize for FileTime { 58 | fn serialize(&self, serializer: S) -> Result 59 | where 60 | S: serde::Serializer, 61 | { 62 | serializer.serialize_str(&format!("{}", self.0)) 63 | } 64 | } 65 | 66 | impl FileTime { 67 | /// Convert the `FileTime` object to a [[]] 68 | pub fn datetime(&self) -> NaiveDateTime { 69 | self.0.to_datetime().naive_utc() 70 | } 71 | 72 | /* 73 | /// Create a new `FileTime` object representing now. 74 | pub fn now() -> Self { 75 | Self::from(chrono::Local::now().naive_local()) 76 | } 77 | */ 78 | } 79 | 80 | impl Default for FileTime { 81 | fn default() -> Self { 82 | let raw = 0u64; 83 | let timestamp = WinTimestamp::new(&raw.to_le_bytes()).unwrap(); 84 | Self(timestamp, raw) 85 | } 86 | } 87 | 88 | /* 89 | impl From for FileTime { 90 | fn from(value: NaiveDateTime) -> Self { 91 | let duration = value - Self::epoch(); 92 | let val = duration.num_microseconds().unwrap() * 10; 93 | let val = val as u64; 94 | Self::from(val) 95 | } 96 | } 97 | */ 98 | 99 | impl From for u64 { 100 | fn from(val: FileTime) -> Self { 101 | val.1 102 | } 103 | } 104 | 105 | #[cfg(test)] 106 | mod tests { 107 | use std::io::Cursor; 108 | 109 | use binrw::{BinReaderExt, BinWrite}; 110 | use winstructs::timestamp::WinTimestamp; 111 | 112 | use super::FileTime; 113 | 114 | #[test] 115 | fn test_guid_be() { 116 | let mut cursor = Cursor::new([0u8; 16]); 117 | let input = test_data(); 118 | 119 | input.write_be(&mut cursor).unwrap(); 120 | cursor.set_position(0); 121 | let output: FileTime = cursor.read_be().unwrap(); 122 | assert_eq!(input.0.to_datetime(), output.0.to_datetime()); 123 | assert_eq!(input.1, output.1); 124 | } 125 | 126 | #[test] 127 | fn test_guid_le() { 128 | let mut cursor = Cursor::new([0u8; 16]); 129 | let input = test_data(); 130 | 131 | input.write_le(&mut cursor).unwrap(); 132 | cursor.set_position(0); 133 | let output: FileTime = cursor.read_le().unwrap(); 134 | assert_eq!(input.0.to_datetime(), output.0.to_datetime()); 135 | assert_eq!(input.1, output.1); 136 | } 137 | 138 | fn test_data() -> FileTime { 139 | let raw = 123456789000u64; 140 | let timestamp = WinTimestamp::new(&raw.to_le_bytes()).unwrap(); 141 | FileTime(timestamp, raw) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/generic_types/guid.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use binrw::{BinRead, BinWrite}; 4 | #[cfg(feature = "serde")] 5 | use serde::Serialize; 6 | use uuid::{Builder, Uuid}; 7 | 8 | /// wraps a UUID 9 | #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] 10 | pub struct Guid(Uuid); 11 | 12 | impl From for Guid { 13 | fn from(uuid: Uuid) -> Self { 14 | Self(uuid) 15 | } 16 | } 17 | 18 | impl BinRead for Guid { 19 | type Args<'a> = (); 20 | 21 | fn read_options( 22 | reader: &mut R, 23 | endian: binrw::Endian, 24 | _args: Self::Args<'_>, 25 | ) -> binrw::BinResult { 26 | let mut bytes = [0; 16]; 27 | reader.read_exact(&mut bytes)?; 28 | let uuid = match endian { 29 | binrw::Endian::Big => Builder::from_bytes(bytes).into_uuid(), 30 | binrw::Endian::Little => Builder::from_bytes_le(bytes).into_uuid(), 31 | }; 32 | Ok(Self(uuid)) 33 | } 34 | } 35 | 36 | impl BinWrite for Guid { 37 | type Args<'a> = (); 38 | 39 | fn write_options( 40 | &self, 41 | writer: &mut W, 42 | endian: binrw::Endian, 43 | _args: Self::Args<'_>, 44 | ) -> binrw::BinResult<()> { 45 | let bytes = match endian { 46 | binrw::Endian::Big => *(self.0.as_bytes()), 47 | binrw::Endian::Little => { 48 | let (mut f1, mut f2, mut f3, f4) = self.0.to_fields_le(); 49 | f1 = u32::from_le_bytes(f1.to_be_bytes()); 50 | f2 = u16::from_le_bytes(f2.to_be_bytes()); 51 | f3 = u16::from_le_bytes(f3.to_be_bytes()); 52 | *(Builder::from_fields_le(f1, f2, f3, f4) 53 | .into_uuid() 54 | .as_bytes()) 55 | } 56 | }; 57 | writer.write_all(&bytes)?; 58 | Ok(()) 59 | } 60 | } 61 | 62 | impl Display for Guid { 63 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 64 | self.0.fmt(f) 65 | } 66 | } 67 | 68 | #[cfg(feature = "serde")] 69 | impl Serialize for Guid { 70 | fn serialize(&self, serializer: S) -> Result 71 | where 72 | S: serde::Serializer, 73 | { 74 | serializer.serialize_str(&self.0.to_string()) 75 | } 76 | } 77 | 78 | #[cfg(test)] 79 | mod tests { 80 | use std::io::Cursor; 81 | 82 | use binrw::{BinReaderExt, BinWrite}; 83 | use uuid::uuid; 84 | 85 | use super::Guid; 86 | 87 | #[test] 88 | fn test_guid_be() { 89 | let mut cursor = Cursor::new([0u8; 16]); 90 | let input = Guid(uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8")); 91 | 92 | input.write_be(&mut cursor).unwrap(); 93 | cursor.set_position(0); 94 | let output: Guid = cursor.read_be().unwrap(); 95 | assert_eq!(input, output); 96 | } 97 | 98 | #[test] 99 | fn test_guid_le() { 100 | let mut cursor = Cursor::new([0u8; 16]); 101 | let input = Guid(uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8")); 102 | 103 | input.write_le(&mut cursor).unwrap(); 104 | cursor.set_position(0); 105 | let output: Guid = cursor.read_le().unwrap(); 106 | assert_eq!(input, output); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/generic_types/idlist.rs: -------------------------------------------------------------------------------- 1 | use binrw::{BinRead, BinReaderExt}; 2 | use getset::Getters; 3 | use log::trace; 4 | use serde::Serialize; 5 | 6 | use crate::itemid::ItemID; 7 | 8 | /// The stored IDList structure specifies the format of a persisted item ID list. 9 | #[derive(Clone, Debug, Default, Getters)] 10 | #[cfg_attr(feature = "serde", derive(Serialize))] 11 | #[getset(get = "pub")] 12 | pub struct IdList { 13 | /// Contains a list of item identifiers. 14 | item_id_list: Vec, 15 | } 16 | 17 | impl BinRead for IdList { 18 | type Args<'a> = (u16,); 19 | 20 | fn read_options( 21 | reader: &mut R, 22 | _endian: binrw::Endian, 23 | args: Self::Args<'_>, 24 | ) -> binrw::BinResult { 25 | let mut item_id_list = Vec::new(); 26 | let mut bytes_to_read = args.0; 27 | trace!("ID List size: {bytes_to_read}"); 28 | while bytes_to_read > 0 { 29 | // an IDList contains any number of ItemID structures, 30 | // followed by TerminalID which has a size of 2 bytes. 31 | // So, if there are less than 2 bytes available, there 32 | // is something wrong 33 | if bytes_to_read < 2 { 34 | return Err(binrw::error::Error::AssertFail { 35 | pos: reader.stream_position()?, 36 | message: "not enough bytes to read".to_string(), 37 | }); 38 | } 39 | 40 | let item_id: ItemID = reader.read_le()?; 41 | 42 | // if the item has a size of zero, then this 43 | // is the terminator 44 | if bytes_to_read == 2 && *item_id.size() == 0 { 45 | break; 46 | } 47 | 48 | bytes_to_read -= item_id.size(); 49 | item_id_list.push(item_id); 50 | } 51 | 52 | Ok(Self { item_id_list }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/generic_types/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod filetime; 2 | pub mod guid; 3 | pub mod idlist; 4 | -------------------------------------------------------------------------------- /src/header.rs: -------------------------------------------------------------------------------- 1 | use binrw::BinRead; 2 | use binrw::BinWrite; 3 | use getset::{Getters, MutGetters, Setters}; 4 | 5 | #[cfg(feature = "serde")] 6 | use serde::Serialize; 7 | 8 | use crate::FileTime; 9 | use crate::Guid; 10 | 11 | mod hotkey_flags; 12 | pub use hotkey_flags::{HotkeyFlags, HotkeyKey, HotkeyModifiers}; 13 | 14 | mod link_flags; 15 | pub use link_flags::LinkFlags; 16 | 17 | mod file_attributes_flags; 18 | pub use file_attributes_flags::FileAttributeFlags; 19 | 20 | /// A ShellLinkHeader structure (section 2.1), which contains identification 21 | /// information, timestamps, and flags that specify the presence of optional 22 | /// structures. 23 | #[derive(Clone, Debug, Getters, MutGetters, Setters)] 24 | #[cfg_attr(feature = "serde", derive(Serialize))] 25 | #[derive(BinRead)] 26 | #[cfg_attr(feature = "binwrite", derive(BinWrite))] 27 | // #[br(little)] 28 | #[getset(get = "pub", get_mut = "pub", set = "pub")] 29 | pub struct ShellLinkHeader { 30 | /// The size, in bytes, of this structure. This value MUST be 0x0000004C. 31 | #[br(assert(header_size == 0x0000_004c))] 32 | header_size: u32, 33 | 34 | /// This value MUST be 00021401-0000-0000-C000-000000000046. 35 | #[br(assert(link_clsid == Guid::from(uuid::uuid!("00021401-0000-0000-C000-000000000046"))))] 36 | link_clsid: Guid, 37 | 38 | /// A LinkFlags structure (section 2.1.1) that specifies information about the shell link and 39 | /// the presence of optional portions of the structure. 40 | link_flags: LinkFlags, 41 | 42 | /// A FileAttributesFlags structure (section 2.1.2) that specifies information about the link 43 | /// target. 44 | file_attributes: FileAttributeFlags, 45 | 46 | /// A FILETIME structure ([MS-DTYP]section 2.3.3) that specifies the creation time of the link 47 | /// target in UTC (Coordinated Universal Time). If the value is zero, there is no creation time 48 | /// set on the link target. 49 | creation_time: FileTime, 50 | 51 | /// A FILETIME structure ([MS-DTYP] section2.3.3) that specifies the access time of the link 52 | /// target in UTC (Coordinated Universal Time). If the value is zero, there is no access time 53 | /// set on the link target. 54 | access_time: FileTime, 55 | 56 | /// A FILETIME structure ([MS-DTYP] section 2.3.3) that specifies the write time of the link 57 | /// target in UTC (Coordinated Universal Time). If the value is zero, there is no write time 58 | /// set on the link target. 59 | write_time: FileTime, 60 | 61 | /// A 32-bit unsigned integer that specifies the size, in bytes, of the link target. If the 62 | /// link target fileis larger than 0xFFFFFFFF, this value specifies the least significant 32 63 | /// bits of the link target file size. 64 | file_size: u32, 65 | 66 | /// A 32-bit signed integer that specifies the index of an icon within a given icon location. 67 | icon_index: i32, 68 | 69 | /// A 32-bit unsigned integer that specifies the expected window state of an application 70 | /// launched by the link. 71 | show_command: ShowCommand, 72 | 73 | /// A HotkeyFlags structure (section 2.1.3) that specifies the keystrokes used to launch the 74 | /// application referenced by the shortcut key. This value is assigned to the application after 75 | /// it is launched, so that pressing the key activates that application. 76 | hotkey: HotkeyFlags, 77 | 78 | /// A value that MUST be zero 79 | #[br(assert(reserved1 == 0))] 80 | #[cfg_attr(feature = "serde", serde(skip))] 81 | reserved1: u16, 82 | 83 | /// A value that MUST be zero 84 | #[br(assert(reserved2 == 0))] 85 | #[cfg_attr(feature = "serde", serde(skip))] 86 | reserved2: u32, 87 | 88 | /// A value that MUST be zero 89 | #[br(assert(reserved3 == 0))] 90 | #[cfg_attr(feature = "serde", serde(skip))] 91 | reserved3: u32, 92 | } 93 | 94 | impl ShellLinkHeader { 95 | /// Set some link flags 96 | pub fn update_link_flags(&mut self, link_flags: LinkFlags, value: bool) { 97 | self.link_flags.set(link_flags, value); 98 | } 99 | } 100 | 101 | impl Default for ShellLinkHeader { 102 | /// Create a new, blank, ShellLinkHeader 103 | fn default() -> Self { 104 | Self { 105 | header_size: 0x4c, 106 | link_clsid: Guid::from(uuid::uuid!("00021401-0000-0000-C000-000000000046")), 107 | link_flags: LinkFlags::IS_UNICODE, 108 | file_attributes: FileAttributeFlags::FILE_ATTRIBUTE_NORMAL, 109 | creation_time: FileTime::default(), 110 | access_time: FileTime::default(), 111 | write_time: FileTime::default(), 112 | file_size: 0, 113 | icon_index: 0, 114 | show_command: ShowCommand::ShowNormal, 115 | hotkey: HotkeyFlags::new(HotkeyKey::NoKeyAssigned, HotkeyModifiers::NO_MODIFIER), 116 | reserved1: 0, 117 | reserved2: 0, 118 | reserved3: 0, 119 | } 120 | } 121 | } 122 | 123 | /// The expected window state of an application launched by the link. 124 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, BinRead, BinWrite)] 125 | #[cfg_attr(feature = "serde", derive(Serialize))] 126 | #[brw(repr=u32)] 127 | pub enum ShowCommand { 128 | /// The application is open and its window is open in a normal fashion. 129 | ShowNormal = 0x01, 130 | /// The application is open, and keyboard focus is given to the application, but its window is 131 | /// not shown. 132 | ShowMaximized = 0x03, 133 | /// The application is open, but its window is not shown. It is not given the keyboard focus. 134 | ShowMinNoActive = 0x07, 135 | } 136 | -------------------------------------------------------------------------------- /src/header/file_attributes_flags.rs: -------------------------------------------------------------------------------- 1 | use bitflags::bitflags; 2 | 3 | #[cfg(feature = "serde")] 4 | use serde::Serialize; 5 | 6 | use crate::binread_flags::binread_flags; 7 | 8 | bitflags! { 9 | /// The FileAttributesFlags structure defines bits that specify the file attributes of the link 10 | /// target, if the target is a file system item. File attributes can be used if the link target 11 | /// is not available, or if accessing the target would be inefficient. It is possible for the 12 | /// target items attributes to be out of sync with this value. 13 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 14 | #[cfg_attr(feature = "serde", derive(Serialize))] 15 | pub struct FileAttributeFlags: u32 { 16 | /// The file or directory is read-only. For a file, if this bit is set, applications can read the file but cannot write to it or delete it. For a directory, if this bit is set, applications cannot delete the directory 17 | const FILE_ATTRIBUTE_READONLY = 0b0000_0000_0000_0000_0000_0000_0000_0001; 18 | /// The file or directory is hidden. If this bit is set, the file or folder is not included in an ordinary directory listing. 19 | const FILE_ATTRIBUTE_HIDDEN = 0b0000_0000_0000_0000_0000_0000_0000_0010; 20 | /// The file or directory is part of the operating system or is used exclusively by the operating system. 21 | const FILE_ATTRIBUTE_SYSTEM = 0b0000_0000_0000_0000_0000_0000_0000_0100; 22 | /// A bit that MUST be zero. 23 | const RESERVED1 = 0b0000_0000_0000_0000_0000_0000_0000_1000; 24 | /// The link target is a directory instead of a file. 25 | const FILE_ATTRIBUTE_DIRECTORY = 0b0000_0000_0000_0000_0000_0000_0001_0000; 26 | /// The file or directory is an archive file. Applications use this flag to mark files for 27 | /// backup or removal. 28 | const FILE_ATTRIBUTE_ARCHIVE = 0b0000_0000_0000_0000_0000_0000_0010_0000; 29 | /// A bit that MUST be zero. 30 | const RESERVED2 = 0b0000_0000_0000_0000_0000_0000_0100_0000; 31 | /// The file or directory has no other flags set. If this bit is 1, all other bits in this 32 | /// structure MUST be clear. 33 | const FILE_ATTRIBUTE_NORMAL = 0b0000_0000_0000_0000_0000_0000_1000_0000; 34 | /// The file is being used for temporary storage. 35 | const FILE_ATTRIBUTE_TEMPORARY = 0b0000_0000_0000_0000_0000_0001_0000_0000; 36 | /// The file is a sparse file. 37 | const FILE_ATTRIBUTE_SPARSE_FILE = 0b0000_0000_0000_0000_0000_0010_0000_0000; 38 | /// The file or directory has an associated reparse point. 39 | const FILE_ATTRIBUTE_REPARSE_POINT = 0b0000_0000_0000_0000_0000_0100_0000_0000; 40 | /// The file or directory is compressed. For a file, this means that all data in the file 41 | /// is compressed. For a directory, this means that compression is the default for newly 42 | /// created files and subdirectories. 43 | const FILE_ATTRIBUTE_COMPRESSED = 0b0000_0000_0000_0000_0000_1000_0000_0000; 44 | /// The data of the file is not immediately available. 45 | const FILE_ATTRIBUTE_OFFLINE = 0b0000_0000_0000_0000_0001_0000_0000_0000; 46 | /// The contents of the file need to be indexed. 47 | const FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = 0b0000_0000_0000_0000_0010_0000_0000_0000; 48 | /// The file or directory is encrypted. For a file, this means that all data in the file is encrypted. For a directory, this means that encryption is the default for newly created files and subdirectories. 49 | const FILE_ATTRIBUTE_ENCRYPTED = 0b0000_0000_0000_0000_0100_0000_0000_0000; 50 | } 51 | } 52 | 53 | binread_flags!(FileAttributeFlags, u32); 54 | -------------------------------------------------------------------------------- /src/header/hotkey_flags.rs: -------------------------------------------------------------------------------- 1 | use binrw::{binrw, BinRead, BinResult, BinWrite, Endian}; 2 | use bitflags::bitflags; 3 | use num_derive::FromPrimitive; 4 | use num_traits::FromPrimitive; 5 | use std::io::{Read, Seek}; 6 | 7 | #[cfg(feature = "serde")] 8 | use serde::Serialize; 9 | 10 | use crate::binread_flags::binread_flags; 11 | 12 | /// The HotkeyFlags structure specifies input generated by a combination of keyboard keys being 13 | /// pressed. 14 | #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] 15 | #[binrw] 16 | #[cfg_attr(feature = "serde", derive(Serialize))] 17 | pub struct HotkeyFlags { 18 | low_byte: HotkeyKey, 19 | high_byte: HotkeyModifiers, 20 | } 21 | 22 | impl HotkeyFlags { 23 | /// Create a new HotkeyFlags instance. 24 | pub fn new(low_byte: HotkeyKey, high_byte: HotkeyModifiers) -> Self { 25 | Self { 26 | low_byte, 27 | high_byte, 28 | } 29 | } 30 | 31 | /// The primary key assigned to the hotkey 32 | pub fn key(&self) -> &HotkeyKey { 33 | &self.low_byte 34 | } 35 | 36 | /// Set the hotkey primary key 37 | pub fn set_key(&mut self, key: HotkeyKey) { 38 | self.low_byte = key; 39 | } 40 | 41 | /// The modifiers (Shift, Ctrl, Alt) for this hotkey 42 | pub fn modifiers(&self) -> &HotkeyModifiers { 43 | &self.high_byte 44 | } 45 | 46 | /// Set the hotkey modifiers (Shift, Ctrl, Alt) 47 | pub fn set_modifiers(&mut self, modifiers: HotkeyModifiers) { 48 | self.high_byte = modifiers; 49 | } 50 | } 51 | 52 | #[allow(missing_docs)] 53 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, FromPrimitive, BinWrite)] 54 | #[cfg_attr(feature = "serde", derive(Serialize))] 55 | /// An 8-bit unsigned integer that specifies a virtual key code that corresponds to a key on the 56 | /// keyboard. 57 | #[bw(repr = u8)] 58 | pub enum HotkeyKey { 59 | NoKeyAssigned = 0x00, 60 | Key0 = 0x30, 61 | Key1, 62 | Key2, 63 | Key3, 64 | Key4, 65 | Key5, 66 | Key6, 67 | Key7, 68 | Key8, 69 | Key9, 70 | KeyA = 0x41, 71 | KeyB, 72 | KeyC, 73 | KeyD, 74 | KeyE, 75 | KeyF, 76 | KeyG, 77 | KeyH, 78 | KeyI, 79 | KeyJ, 80 | KeyK, 81 | KeyL, 82 | KeyM, 83 | KeyN, 84 | KeyO, 85 | KeyP, 86 | KeyQ, 87 | KeyR, 88 | KeyS, 89 | KeyT, 90 | KeyU, 91 | KeyV, 92 | KeyW, 93 | KeyX, 94 | KeyY, 95 | KeyZ, 96 | F1 = 0x70, 97 | F2, 98 | F3, 99 | F4, 100 | F5, 101 | F6, 102 | F7, 103 | F8, 104 | F9, 105 | F10, 106 | F11, 107 | F12, 108 | F13, 109 | F14, 110 | F15, 111 | F16, 112 | F17, 113 | F18, 114 | F19, 115 | F20, 116 | F21, 117 | F22, 118 | F23, 119 | F24, 120 | NumLock = 0x90, 121 | ScrollLock, 122 | } 123 | 124 | // Custom BinRead implementation to catch invalid hotkey key from bad generated shortcuts 125 | impl BinRead for HotkeyKey { 126 | type Args<'a> = (); 127 | 128 | fn read_options( 129 | reader: &mut R, 130 | options: Endian, 131 | _: Self::Args<'_>, 132 | ) -> BinResult { 133 | let value = u8::read_options(reader, options, ())?; 134 | Ok(HotkeyKey::from_u8(value).unwrap_or(HotkeyKey::NoKeyAssigned)) 135 | } 136 | } 137 | 138 | bitflags! { 139 | /// An 8-bit unsigned integer that specifies bits that correspond to modifier keys on the 140 | /// keyboard 141 | /// 142 | #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] 143 | #[cfg_attr(feature = "serde", derive(Serialize))] 144 | pub struct HotkeyModifiers: u8 { 145 | /// No modifier key is being used. 146 | const NO_MODIFIER = 0x00; 147 | /// The "SHIFT" key on the keyboard. 148 | const HOTKEYF_SHIFT = 0x01; 149 | /// The "CTRL" key on the keyboard. 150 | const HOTKEYF_CONTROL = 0x02; 151 | /// The "ALT" key on the keyboard. 152 | const HOTKEYF_ALT = 0x04; 153 | } 154 | } 155 | 156 | binread_flags!(HotkeyModifiers, u8); 157 | -------------------------------------------------------------------------------- /src/header/link_flags.rs: -------------------------------------------------------------------------------- 1 | use bitflags::bitflags; 2 | 3 | #[cfg(feature = "serde")] 4 | use serde::Serialize; 5 | 6 | use crate::binread_flags::binread_flags; 7 | 8 | bitflags! { 9 | /// The LinkFlags structure defines bits that specify which shell linkstructures are present in 10 | /// the file format after the ShellLinkHeaderstructure (section 2.1). 11 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 12 | #[cfg_attr(feature = "serde", derive(Serialize))] 13 | pub struct LinkFlags: u32 { 14 | /// The shell link is saved with an item ID list (IDList). If this bit is set, a 15 | /// LinkTargetIDList structure (section 2.2) MUST follow the ShellLinkHeader. If this bit 16 | /// is not set, this structure MUST NOT be present. 17 | const HAS_LINK_TARGET_ID_LIST = 0b0000_0000_0000_0000_0000_0000_0000_0001; 18 | /// The shell link is saved with link information. If this bit is set, a LinkInfo structure 19 | /// (section 2.3) MUST be present. If this bit is not set, this structure MUST NOT be 20 | /// present. 21 | const HAS_LINK_INFO = 0b0000_0000_0000_0000_0000_0000_0000_0010; 22 | /// The shell link is saved with a name string. If this bit is set, a NAME_STRING 23 | /// StringData structure (section 2.4) MUST be present. If this bit is not set, this 24 | /// structure MUST NOT be present. 25 | const HAS_NAME = 0b0000_0000_0000_0000_0000_0000_0000_0100; 26 | /// The shell link is saved with a relative path string. If this bit is set, a 27 | /// RELATIVE_PATH StringData structure (section 2.4) MUST be present. If this bit is not 28 | /// set, this structure MUST NOT be present. 29 | const HAS_RELATIVE_PATH = 0b0000_0000_0000_0000_0000_0000_0000_1000; 30 | /// The shell link is saved with a relative path string. If this bit is set, a 31 | /// WORKING_DIR StringData structure (section 2.4) MUST be present. If this bit is not 32 | /// set, this structure MUST NOT be present. 33 | const HAS_WORKING_DIR = 0b0000_0000_0000_0000_0000_0000_0001_0000; 34 | /// The shell link is saved with a relative path string. If this bit is set, a 35 | /// COMMAND_LINE_ARGUMENTS StringData structure (section 2.4) MUST be present. If this bit 36 | /// is not set, this structure MUST NOT be present. 37 | const HAS_ARGUMENTS = 0b0000_0000_0000_0000_0000_0000_0010_0000; 38 | /// The shell link is saved with a relative path string. If this bit is set, a 39 | /// ICON_LOCATION StringData structure (section 2.4) MUST be present. If this bit is not 40 | /// set, this structure MUST NOT be present. 41 | const HAS_ICON_LOCATION = 0b0000_0000_0000_0000_0000_0000_0100_0000; 42 | /// The shell link contains Unicode encoded strings. This bit SHOULD be set. If this bit is 43 | /// set, the StringData section contains Unicode-encoded strings; otherwise, it contains 44 | /// strings that are encoded using the system default code page 45 | const IS_UNICODE = 0b0000_0000_0000_0000_0000_0000_1000_0000; 46 | /// The LinkInfo structure (section 2.3) is ignored. 47 | const FORCE_NO_LINK_INFO = 0b0000_0000_0000_0000_0000_0001_0000_0000; 48 | /// The shell link is saved with an EnvironmentVariableDataBlock (section 2.5.4). 49 | const HAS_EXP_STRING = 0b0000_0000_0000_0000_0000_0010_0000_0000; 50 | /// The target is run in a separate virtual machine when launching a link target that is a 51 | /// 16-bit application. 52 | const RUN_IN_SEPARATE_PROCESS = 0b0000_0000_0000_0000_0000_0100_0000_0000; 53 | /// A bit that is undefined and MUST be ignored. 54 | const UNUSED1 = 0b0000_0000_0000_0000_0000_1000_0000_0000; 55 | /// The shell link is saved with a DarwinDataBlock(section2.5.3). 56 | const HAS_DARWIN_ID = 0b0000_0000_0000_0000_0001_0000_0000_0000; 57 | /// The application is run as a different user when the target of the shell link is 58 | /// activated. 59 | const RUN_AS_USER = 0b0000_0000_0000_0000_0010_0000_0000_0000; 60 | /// The shell link is saved with an IconEnvironmentDataBlock (section 2.5.5). 61 | const HAS_EXP_ICON = 0b0000_0000_0000_0000_0100_0000_0000_0000; 62 | /// The file system location is represented in the shell namespace when the path to an item 63 | /// is parsed into an IDList. 64 | const NO_PIDL_ALIAS = 0b0000_0000_0000_0000_1000_0000_0000_0000; 65 | /// A bit that is undefined and MUST be ignored. 66 | const UNUSED2 = 0b0000_0000_0000_0001_0000_0000_0000_0000; 67 | /// The shell link is saved with a ShimDataBlock(section2.5.8) 68 | const RUN_WITH_SHIM_LAYER = 0b0000_0000_0000_0010_0000_0000_0000_0000; 69 | /// The TrackerDataBlock(section2.5.10)is ignored. 70 | const FORCE_NO_LINK_TRACK = 0b0000_0000_0000_0100_0000_0000_0000_0000; 71 | /// The shell link attempts to collect target properties and store them in the 72 | /// PropertyStoreDataBlock(section2.5.7) when the link target is set. 73 | const ENABLE_TARGET_METADATA = 0b0000_0000_0000_1000_0000_0000_0000_0000; 74 | /// The EnvironmentVariableDataBlock is ignored. 75 | const DISABLE_LINK_PATH_TRACKING = 0b0000_0000_0001_0000_0000_0000_0000_0000; 76 | /// The SpecialFolderDataBlock(section2.5.9)and the KnownFolderDataBlock(section2.5.6)are 77 | /// ignored when loading the shell link. If this bit is set, these extra data blocks SHOULD 78 | /// NOT be saved when saving the shell link. 79 | const DISABLE_KNOWN_FOLDER_TRACKING = 0b0000_0000_0010_0000_0000_0000_0000_0000; 80 | /// If the linkhas a KnownFolderDataBlock(section2.5.6), the unaliased form of the known 81 | /// folder IDList SHOULD be used when translating the target IDList at the time that the 82 | /// link is loaded. 83 | const DISABLE_KNOWN_FOLDER_ALIAS = 0b0000_0000_0100_0000_0000_0000_0000_0000; 84 | /// Creating a link that references another link is enabled. Otherwise, specifying a link 85 | /// as the target IDList SHOULD NOT be allowed. 86 | const ALLOW_LINK_TO_LINK = 0b0000_0000_1000_0000_0000_0000_0000_0000; 87 | /// When saving a link for which the target IDList is under a known folder, either the 88 | /// unaliased form of that known folder or the target IDList SHOULD be used. 89 | const UNALIAS_ON_SAVE = 0b0000_0001_0000_0000_0000_0000_0000_0000; 90 | /// The target IDList SHOULD NOT be stored; instead, the path specified in the 91 | /// EnvironmentVariableDataBlock(section2.5.4) SHOULD be used to refer to the target. 92 | const PREFER_ENVIRONMENT_PATH = 0b0000_0010_0000_0000_0000_0000_0000_0000; 93 | /// When the target is a UNC name that refers to a location on a local machine, the local 94 | /// path IDList in the PropertyStoreDataBlock(section2.5.7) SHOULD be stored, so it can be 95 | /// used when the link is loaded on the local machine. 96 | const KEEP_LOCAL_ID_LIST_FOR_UNC_TARGET = 0b0000_0100_0000_0000_0000_0000_0000_0000; 97 | } 98 | } 99 | 100 | binread_flags!(LinkFlags, u32); 101 | -------------------------------------------------------------------------------- /src/itemid.rs: -------------------------------------------------------------------------------- 1 | use core::fmt; 2 | 3 | use binrw::BinRead; 4 | use getset::Getters; 5 | use serde::Serialize; 6 | 7 | /// The stored IDList structure specifies the format of a persisted item ID list. 8 | #[derive(Clone, BinRead, Default, Getters)] 9 | #[cfg_attr(feature = "serde", derive(Serialize))] 10 | #[getset(get = "pub")] 11 | pub struct ItemID { 12 | /// A 16-bit, unsigned integer that specifies the size, in bytes, of the ItemID structure, 13 | /// including the ItemIDSize field. 14 | #[br(assert(size == 0 || size>2))] 15 | #[cfg_attr(feature = "serde", serde(skip))] 16 | size: u16, 17 | 18 | /// The shell data source-defined data that specifies an item. 19 | #[br(if(size > 0), count=if size > 0 {size - 2} else {0})] 20 | data: Vec, 21 | } 22 | 23 | impl fmt::Debug for ItemID { 24 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 25 | write!(f, "ItemID (raw data size {})", self.size) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/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 | 36 | use binrw::BinReaderExt; 37 | use getset::{Getters, MutGetters}; 38 | #[allow(unused)] 39 | use log::{debug, error, info, trace, warn}; 40 | #[cfg(feature = "serde")] 41 | use serde::Serialize; 42 | 43 | use std::io::BufReader; 44 | #[cfg(feature = "binwrite")] 45 | use std::io::BufWriter; 46 | use std::path::Path; 47 | use std::{fs::File, io::Seek}; 48 | 49 | mod header; 50 | pub 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). 58 | pub mod linktarget; 59 | pub 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 67 | pub mod linkinfo; 68 | pub use linkinfo::LinkInfo; 69 | 70 | mod stringdata; 71 | pub use stringdata::StringData; 72 | 73 | /// Structures from the ExtraData section of the Shell Link. 74 | pub mod extradata; 75 | pub use extradata::ExtraData; 76 | 77 | mod generic_types; 78 | pub use generic_types::filetime::FileTime; 79 | pub use generic_types::guid::*; 80 | pub use generic_types::idlist::*; 81 | 82 | mod current_offset; 83 | pub use current_offset::*; 84 | 85 | mod strings; 86 | pub use strings::*; 87 | 88 | mod itemid; 89 | pub use itemid::*; 90 | 91 | #[macro_use] 92 | mod binread_flags; 93 | 94 | mod error; 95 | pub 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")] 101 | pub 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, 108 | 109 | /// returns the [`LinkInfo`] structure 110 | link_info: Option, 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 | 125 | impl 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 | 147 | impl ShellLink { 148 | /// Create a new ShellLink pointing to a location, with otherwise default settings. 149 | pub fn new_simple>(to: P) -> std::io::Result { 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>(&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 = 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 = 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 | /// > () 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>( 325 | path: P, 326 | encoding: crate::strings::Encoding, 327 | ) -> Result { 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 { 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) { 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) { 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) { 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) { 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) { 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 | } 484 | -------------------------------------------------------------------------------- /src/linkinfo.rs: -------------------------------------------------------------------------------- 1 | use core::panic; 2 | 3 | use binrw::BinRead; 4 | use bitflags::bitflags; 5 | use encoding_rs::Encoding; 6 | use getset::Getters; 7 | 8 | use crate::{ 9 | binread_flags::binread_flags, 10 | strings::{NullTerminatedString, StringEncoding}, 11 | CurrentOffset, 12 | }; 13 | 14 | #[cfg(feature = "serde")] 15 | use serde::Serialize; 16 | 17 | /// The LinkInfo structure specifies information necessary to resolve a 18 | /// linktarget if it is not found in its original location. This includes 19 | /// information about the volume that the target was stored on, the mapped 20 | /// drive letter, and a Universal Naming Convention (UNC)form of the path 21 | /// if one existed when the linkwas created. For more details about UNC 22 | /// paths, see [MS-DFSNM] section 2.2.1.4 23 | #[derive(Debug, BinRead, Getters)] 24 | #[cfg_attr(feature = "serde", derive(Serialize))] 25 | #[getset(get = "pub")] 26 | #[allow(unused)] 27 | #[br(import(default_codepage: &'static Encoding))] 28 | pub struct LinkInfo { 29 | /// stores the beginning og this data structure 30 | #[serde(skip)] 31 | #[getset(skip)] 32 | link_info_offset: CurrentOffset, 33 | /// LinkInfoSize (4 bytes): A 32-bit, unsigned integer that specifies the 34 | /// size, in bytes, of the LinkInfo structure. All offsets specified in 35 | /// this structure MUST be less than this value, and all strings contained 36 | /// in this structure MUST fit within the extent defined by this size. 37 | link_info_size: u32, 38 | 39 | /// LinkInfoHeaderSize (4 bytes): A 32-bit, unsigned integer that 40 | /// specifies the size, in bytes, of the LinkInfo header section, which is 41 | /// composed of the LinkInfoSize, LinkInfoHeaderSize, LinkInfoFlags, 42 | /// VolumeIDOffset, LocalBasePathOffset, CommonNetworkRelativeLinkOffset, 43 | /// CommonPathSuffixOffset fields, and, if included, the 44 | /// LocalBasePathOffsetUnicode and CommonPathSuffixOffsetUnicode fields. 45 | link_info_header_size: u32, 46 | 47 | /// Flags that specify whether the VolumeID, LocalBasePath, 48 | /// LocalBasePathUnicode, and CommonNetworkRelativeLinkfields are present 49 | /// in this structure. 50 | link_info_flags: LinkInfoFlags, 51 | 52 | /// VolumeIDOffset (4 bytes): A 32-bit, unsigned integer that specifies the 53 | /// location of the VolumeID field. If the VolumeIDAndLocalBasePath flag is 54 | /// set, this value is an offset, in bytes, from the start of the LinkInfo 55 | /// structure; otherwise, this value MUST be zero. 56 | #[br( 57 | assert( 58 | if link_info_flags.has_volume_id_and_local_base_path() { 59 | volume_id_offset >= link_info_header_size && 60 | volume_id_offset < link_info_size 61 | } else { 62 | true // we handle the other case as non-fatal error 63 | } 64 | ), 65 | map = |x: u32| { 66 | if link_info_flags.has_volume_id_and_local_base_path() { 67 | x 68 | } else { 69 | if x != 0 { 70 | log::warn!("The VolumeIDAndLocalBasePath flag was not set, \ 71 | but the VolumeID field is not set to zero (actual value is \ 72 | 0x{x:08x}). I set it to be zero."); 73 | } 74 | 0 75 | } 76 | } 77 | )] 78 | volume_id_offset: u32, 79 | 80 | /// LocalBasePathOffset (4 bytes): A 32-bit, unsigned integer that 81 | /// specifies the location of the LocalBasePath field. If the 82 | /// VolumeIDAndLocalBasePath flag is set, this value is an offset, in 83 | /// bytes, from the start of the LinkInfo structure; otherwise, this value 84 | /// MUST be zero. 85 | #[br( 86 | assert( 87 | if link_info_flags.has_volume_id_and_local_base_path() { 88 | local_base_path_offset >= link_info_header_size && 89 | local_base_path_offset < link_info_size 90 | } else { 91 | true // we handle the other case as non-fatal error 92 | } 93 | ), 94 | map = |x: u32| { 95 | if link_info_flags.has_volume_id_and_local_base_path() { 96 | x 97 | } else { 98 | if x != 0 { 99 | log::warn!("The VolumeIDAndLocalBasePath flag was not set, \ 100 | but the LocalBasePath field is not set to zero (actual \ 101 | value is 0x{x:08x}). I set it to be zero."); 102 | } 103 | 0 104 | } 105 | } 106 | )] 107 | local_base_path_offset: u32, 108 | 109 | /// CommonNetworkRelativeLinkOffset (4 bytes): A 32-bit, unsigned integer 110 | /// that specifies the location of the CommonNetworkRelativeLink field. If 111 | /// the CommonNetworkRelativeLinkAndPathSuffix flag is set, this value is 112 | /// an offset, in bytes, from the start of the LinkInfo structure; 113 | /// otherwise, this value MUST be zero. 114 | #[br( 115 | assert( 116 | if link_info_flags.has_common_network_relative_link_and_path_suffix() { 117 | common_network_relative_link_offset >= link_info_header_size && 118 | common_network_relative_link_offset < link_info_size 119 | } else { 120 | true // we handle the other case as non-fatal error 121 | } 122 | ), 123 | map = |x: u32| { 124 | if link_info_flags.has_common_network_relative_link_and_path_suffix() { 125 | x 126 | } else { 127 | if x != 0 { 128 | log::warn!("The CommonNetworkRelativeLinkAndPathSuffix flag \ 129 | was not set, but the CommonNetworkRelativeLinkOffset field \ 130 | is not set to zero (actual value is 0x{x:08x}). I set it to \ 131 | be zero."); 132 | } 133 | 0 134 | } 135 | } 136 | )] 137 | common_network_relative_link_offset: u32, 138 | 139 | /// CommonPathSuffixOffset (4 bytes): A 32-bit, unsigned integer that 140 | /// specifies the location of the CommonPathSuffix field. This value is 141 | /// an offset, in bytes, from the start of the LinkInfo structure. 142 | #[br(assert(common_path_suffix_offset < link_info_size && 143 | common_path_suffix_offset >= link_info_header_size))] 144 | common_path_suffix_offset: u32, 145 | 146 | /// LocalBasePathOffsetUnicode (4 bytes): An optional, 32-bit, unsigned 147 | /// integer that specifies the location of the LocalBasePathUnicode field. 148 | /// If the VolumeIDAndLocalBasePath flag is set, this value is an offset, 149 | /// in bytes, from the start of the LinkInfo structure; otherwise, this 150 | /// value MUST be zero. This field can be present only if the value of the 151 | /// LinkInfoHeaderSize field is greater than or equal to 0x00000024. 152 | #[br( 153 | if(link_info_header_size >= 0x24), 154 | assert( 155 | if let Some(offset) = local_base_path_offset_unicode { 156 | if link_info_flags.has_volume_id_and_local_base_path() { 157 | offset >= link_info_header_size && offset < link_info_size 158 | } else { 159 | true // we handle the other case as non-fatal error 160 | } 161 | } else { 162 | true // this link doesn't handle unicode 163 | }, 164 | "offset has unexpected value: {local_base_path_offset_unicode:?}" 165 | ), 166 | map = |x: Option| x.map(|x: u32| { 167 | if link_info_flags.has_volume_id_and_local_base_path() { 168 | x 169 | } else { 170 | if x != 0 { 171 | log::warn!("The VolumeIDAndLocalBasePath flag was not set,\ 172 | but the LocalBasePathOffsetUnicode field is not set to zero\ 173 | (actual value is 0x{x:08x}). I set it to be zero."); 174 | } 175 | 0 176 | } 177 | }) 178 | )] 179 | local_base_path_offset_unicode: Option, 180 | 181 | /// CommonPathSuffixOffsetUnicode (4 bytes): An optional, 32-bit, unsigned 182 | /// integer that specifies the location of the CommonPathSuffixUnicode 183 | /// field. This value is an offset, in bytes, from the start of the 184 | /// LinkInfo structure. This field can be present only if the value of the 185 | /// LinkInfoHeaderSize field is greater than or equal to 0x00000024. 186 | #[br( 187 | if(link_info_header_size >= 0x24), 188 | assert ( 189 | if let Some(offset) = common_path_suffix_offset_unicode { 190 | offset > link_info_header_size && offset < link_info_size 191 | } else {true} 192 | ) 193 | )] 194 | common_path_suffix_offset_unicode: Option, 195 | 196 | /// An optional VolumeID structure (section 2.3.1) that specifies 197 | /// information about the volume that the link target was on when the link 198 | /// was created. This field is present if the VolumeIDAndLocalBasePath 199 | /// flag is set. 200 | #[br( 201 | if(link_info_flags.has_volume_id_and_local_base_path()), 202 | args(default_codepage), 203 | seek_before(binrw::io::SeekFrom::Start((link_info_offset.as_ref() + volume_id_offset).into())) 204 | )] 205 | volume_id: Option, 206 | 207 | /// An optional, NULL–terminated string, defined by the system default code 208 | /// page, which is used to construct the full path to the link item or link 209 | /// target by appending the string in the CommonPathSuffix field. This 210 | /// field is present if the VolumeIDAndLocalBasePath flag is set. 211 | #[br( 212 | if(link_info_flags.has_volume_id_and_local_base_path()), 213 | args(StringEncoding::CodePage(default_codepage)), 214 | map=|o: Option| o.map(|n| n.to_string()), 215 | seek_before(binrw::io::SeekFrom::Start((link_info_offset.as_ref() + local_base_path_offset).into())) 216 | )] 217 | #[getset(skip)] 218 | local_base_path: Option, 219 | 220 | /// An optional CommonNetworkRelativeLink structure (section 2.3.2) that 221 | /// specifies information about the network location where the link target 222 | /// is stored. 223 | #[br( 224 | if(link_info_flags.has_common_network_relative_link_and_path_suffix()), 225 | args(default_codepage), 226 | seek_before(binrw::io::SeekFrom::Start((link_info_offset.as_ref() + common_network_relative_link_offset).into())) 227 | )] 228 | common_network_relative_link: Option, 229 | 230 | /// A NULL–terminated string, defined by the system default code page, 231 | /// which is used to construct the full path to the link item or link 232 | /// target by being appended to the string in the LocalBasePath field. 233 | #[br( 234 | if(common_path_suffix_offset != 0), 235 | args(StringEncoding::CodePage(default_codepage)), 236 | map=|n: NullTerminatedString| n.to_string(), 237 | seek_before(binrw::io::SeekFrom::Start((link_info_offset.as_ref() + common_path_suffix_offset).into())) 238 | )] 239 | #[getset(skip)] 240 | common_path_suffix: String, 241 | 242 | /// An optional, NULL–terminated, Unicode string that is used to construct 243 | /// the full path to the link item or link target by appending the string 244 | /// in the CommonPathSuffixUnicode field. This field can be present only 245 | /// if the VolumeIDAndLocalBasePath flag is set and the value of the 246 | /// LinkInfoHeaderSize field is greater than or equal to 0x00000024. 247 | #[br( 248 | if(link_info_header_size >= 0x24 && link_info_flags.has_volume_id_and_local_base_path()), 249 | args(StringEncoding::Unicode), 250 | map=|o: Option| o.map(|n| n.to_string()), 251 | seek_before(binrw::io::SeekFrom::Start((link_info_offset.as_ref() + local_base_path_offset_unicode.unwrap()).into())) 252 | )] 253 | local_base_path_unicode: Option, 254 | 255 | /// An optional, NULL–terminated, Unicode string that is used to construct 256 | /// the full path to the link item or link target by being appended to the 257 | /// string in the LocalBasePathUnicode field. This field can be present 258 | /// only if the value of the LinkInfoHeaderSize field is greater than or 259 | /// equal to 0x00000024. 260 | #[br( 261 | if(link_info_header_size >= 0x24 && common_path_suffix_offset_unicode.map(|o| o != 0).unwrap_or(false)), 262 | args(StringEncoding::Unicode), 263 | map=|o: Option| o.map(|n| n.to_string()), 264 | seek_before(binrw::io::SeekFrom::Start((link_info_offset.as_ref() + common_path_suffix_offset_unicode.unwrap()).into())) 265 | )] 266 | common_path_suffix_unicode: Option, 267 | } 268 | 269 | impl LinkInfo { 270 | /// An optional, NULL–terminated string, defined by the system default code 271 | /// page, which is used to construct the full path to the link item or link 272 | /// target by appending the string in the CommonPathSuffix field. This 273 | /// field is present if the VolumeIDAndLocalBasePath flag is set. 274 | pub fn local_base_path(&self) -> Option<&str> { 275 | self.local_base_path.as_ref().map(|x| x.as_ref()) 276 | } 277 | /// A NULL–terminated string, defined by the system default code page, 278 | /// which is used to construct the full path to the link item or link 279 | /// target by being appended to the string in the LocalBasePath field. 280 | pub fn common_path_suffix(&self) -> &str { 281 | self.common_path_suffix.as_ref() 282 | } 283 | } 284 | 285 | impl From for Vec { 286 | fn from(_val: LinkInfo) -> Self { 287 | unimplemented!() 288 | } 289 | } 290 | 291 | bitflags! { 292 | /// Flags that specify whether the VolumeID, LocalBasePath, LocalBasePathUnicode, 293 | /// and CommonNetworkRelativeLink fields are present in this structure. 294 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 295 | #[cfg_attr(feature = "serde", derive(Serialize))] 296 | pub struct LinkInfoFlags: u32 { 297 | /// If set, the VolumeIDand LocalBasePath fields are present, and their 298 | /// locations are specified by the values of the VolumeIDOffset and 299 | /// LocalBasePathOffset fields, respectively. If the value of the 300 | /// LinkInfoHeaderSize field is greater than or equal to 0x00000024, the 301 | /// LocalBasePathUnicode field is present, and its location is specified 302 | /// by the value of the LocalBasePathOffsetUnicode field. If not set, 303 | /// the VolumeID, LocalBasePath, and LocalBasePathUnicode fields are 304 | /// not present, and the values of the VolumeIDOffset and 305 | /// LocalBasePathOffset fields are zero. If the value of the 306 | /// LinkInfoHeaderSize field is greater than or equal to 0x00000024, 307 | /// the value of the LocalBasePathOffsetUnicode field is zero. 308 | const VOLUME_ID_AND_LOCAL_BASE_PATH = 0b0000_0000_0000_0000_0000_0000_0000_0001; 309 | 310 | /// If set, the CommonNetworkRelativeLink field is present, and its 311 | /// location is specified by the value of the 312 | /// CommonNetworkRelativeLinkOffset field.If not set, the 313 | /// CommonNetworkRelativeLink field is not present, and the value of 314 | /// the CommonNetworkRelativeLinkOffset field is zero 315 | const COMMON_NETWORK_RELATIVE_LINK_AND_PATH_SUFFIX = 0b0000_0000_0000_0000_0000_0000_0000_0010; 316 | } 317 | } 318 | 319 | binread_flags!(LinkInfoFlags, u32); 320 | 321 | #[allow(missing_docs)] 322 | impl LinkInfoFlags { 323 | pub fn has_volume_id_and_local_base_path(&self) -> bool { 324 | self.contains(Self::VOLUME_ID_AND_LOCAL_BASE_PATH) 325 | } 326 | 327 | pub fn has_common_network_relative_link_and_path_suffix(&self) -> bool { 328 | self.contains(Self::COMMON_NETWORK_RELATIVE_LINK_AND_PATH_SUFFIX) 329 | } 330 | } 331 | 332 | /// The VolumeID structure specifies information about the volume that a link 333 | /// target was on when the link was created. This information is useful for 334 | /// resolving the link if the file is not found in its original location. 335 | #[derive(Clone, Debug, BinRead, Getters)] 336 | #[cfg_attr(feature = "serde", derive(Serialize))] 337 | #[get(get = "pub")] 338 | #[allow(unused)] 339 | #[br(import(default_codepage: &'static Encoding))] 340 | pub struct VolumeID { 341 | #[get(skip)] 342 | #[cfg_attr(feature = "serde", serde(skip))] 343 | start_offset: CurrentOffset, 344 | /// VolumeIDSize (4 bytes): A 32-bit, unsigned integer that specifies the 345 | /// size, in bytes, of this structure. This value MUST be greater than 346 | /// `0x00000010``. All offsets specified in this structure MUST be less 347 | /// than this value, and all strings contained in this structure MUST fit 348 | /// within the extent defined by this size. 349 | #[br(assert(volume_id_size > 0x10))] 350 | volume_id_size: u32, 351 | 352 | /// A 32-bit, unsigned integer that specifies the type of drive the link 353 | /// target is stored on. 354 | drive_type: DriveType, 355 | 356 | /// A 32-bit, unsigned integer that specifies the drive serial number of 357 | /// the volume the link target is stored on. 358 | drive_serial_number: u32, 359 | 360 | /// VolumeLabelOffset (4 bytes): A 32-bit, unsigned integer that 361 | /// specifies the location of a string that contains the volume label of 362 | /// the drive that the link target is stored on. This value is an offset, 363 | /// in bytes, from the start of the VolumeID structure to a NULL-terminated 364 | /// string of characters, defined by the system default code page. The 365 | /// volume label string is located in the Data field of this structure. 366 | /// 367 | /// If the value of this field is 0x00000014, it MUST be ignored, and the 368 | /// value of the VolumeLabelOffsetUnicode field MUST be used to locate the 369 | /// volume label string. 370 | #[br(assert(volume_label_offset < volume_id_size))] 371 | volume_label_offset: u32, 372 | 373 | /// VolumeLabelOffsetUnicode (4 bytes): An optional, 32-bit, unsigned 374 | /// integer that specifies the location of a string that contains the 375 | /// volume label of the drive that the link target is stored on. This value 376 | /// is an offset, in bytes, from the start of the VolumeID structure to a 377 | /// NULL-terminated string of Unicode characters. The volume label string 378 | /// is located in the Data field of this structure. 379 | /// 380 | /// If the value of the VolumeLabelOffset field is not 0x00000014, this 381 | /// field MUST NOT be present; instead, the value of the VolumeLabelOffset 382 | /// field MUST be used to locate the volume label string. 383 | #[br(if(volume_label_offset == 0x14))] 384 | volume_label_offset_unicode: Option, 385 | 386 | /// The label of the volume that the link target is stored on. 387 | #[br( 388 | args({volume_label_offset_unicode.and(Some(StringEncoding::Unicode)).unwrap_or(StringEncoding::CodePage(default_codepage))}), 389 | map=|s: NullTerminatedString| s.to_string() 390 | )] 391 | #[getset(skip)] 392 | volume_label: String, 393 | 394 | #[cfg_attr(feature = "serde", serde(skip))] 395 | _next_offset: CurrentOffset, 396 | } 397 | 398 | impl VolumeID { 399 | /// The label of the volume that the link target is stored on. 400 | pub fn volume_label(&self) -> &str { 401 | self.volume_label.as_ref() 402 | } 403 | } 404 | 405 | impl From for Vec { 406 | fn from(_val: VolumeID) -> Self { 407 | unimplemented!() 408 | } 409 | } 410 | 411 | /// A 32-bit, unsigned integer that specifies the type of drive the link target is stored on. 412 | #[derive(Clone, Debug, BinRead)] 413 | #[cfg_attr(feature = "serde", derive(Serialize))] 414 | #[br(repr(u32))] 415 | pub enum DriveType { 416 | /// The drive type cannot be determined. 417 | DriveUnknown = 0x00, 418 | /// The root path is invalid; for example, there is no volume mounted at the path. 419 | DriveNoRootDir = 0x01, 420 | /// The drive has removable media, such as a floppy drive, thumb drive, or flash card reader. 421 | DriveRemovable = 0x02, 422 | /// The drive has fixed media, such as a hard drive or flash drive. 423 | DriveFixed = 0x03, 424 | /// The drive is a remote (network) drive. 425 | DriveRemote = 0x04, 426 | /// The drive is a CD-ROM drive. 427 | DriveCDRom = 0x05, 428 | /// The drive is a RAM disk. 429 | DriveRamdisk = 0x06, 430 | } 431 | 432 | /// The CommonNetworkRelativeLink structure specifies information about the network location where a 433 | /// link target is stored, including the mapped drive letter and the UNC path prefix. For details on 434 | /// UNC paths, see [MS-DFSNM] section 2.2.1.4. 435 | /// 436 | /// 437 | #[derive(Clone, Debug, BinRead)] 438 | #[cfg_attr(feature = "serde", derive(Serialize))] 439 | #[allow(unused)] 440 | #[br(import(default_codepage: &'static Encoding))] 441 | pub struct CommonNetworkRelativeLink { 442 | #[serde(skip)] 443 | /// CommonNetworkRelativeLinkSize (4 bytes): A 32-bit, unsigned integer 444 | /// that specifies the size, in bytes, of the CommonNetworkRelativeLink 445 | /// structure. This value MUST be greater than or equal to 0x00000014. All 446 | /// offsets specified in this structure MUST be less than this value, and 447 | /// all strings contained in this structure MUST fit within the extent 448 | /// defined by this size. 449 | #[br(assert(common_network_relative_link_size >= 0x14))] 450 | common_network_relative_link_size: u32, 451 | 452 | /// Flags that specify the contents of the DeviceNameOffset and 453 | /// NetProviderType fields. 454 | flags: CommonNetworkRelativeLinkFlags, 455 | 456 | /// NetNameOffset (4 bytes): A 32-bit, unsigned integer that specifies the 457 | /// location of the NetName field. This value is an offset, in bytes, from 458 | /// the start of the CommonNetworkRelativeLink structure. 459 | #[br(assert(net_name_offset < common_network_relative_link_size))] 460 | net_name_offset: u32, 461 | 462 | /// DeviceNameOffset (4 bytes): A 32-bit, unsigned integer that specifies 463 | /// the location of the DeviceName field. If the ValidDevice flag is set, 464 | /// this value is an offset, in bytes, from the start of the 465 | /// CommonNetworkRelativeLink structure; otherwise, this value MUST be 466 | /// zero. 467 | #[br( 468 | assert( 469 | device_name_offset < common_network_relative_link_size && 470 | if flags.has_valid_device() { 471 | device_name_offset > 0 472 | } else { 473 | device_name_offset == 0 474 | } 475 | ) 476 | )] 477 | device_name_offset: u32, 478 | 479 | /// NetworkProviderType (4 bytes): A 32-bit, unsigned integer that 480 | /// specifies the type of network provider. If the ValidNetType flag is 481 | /// set, this value MUST be one of the following; otherwise, this value 482 | /// MUST be ignored. 483 | #[br(map = |t| if flags.has_valid_net_type() {Some(t)} else {None})] 484 | network_provider_type: Option, 485 | 486 | /// NetNameOffsetUnicode (4 bytes): An optional, 32-bit, unsigned integer 487 | /// that specifies the location of the NetNameUnicode field. This value is 488 | /// an offset, in bytes, from the start of the CommonNetworkRelativeLink 489 | /// structure. This field MUST be present if the value of the NetNameOffset 490 | /// field is greater than 0x00000014; otherwise, this field MUST NOT be present. 491 | #[br(if(net_name_offset > 0x00000014))] 492 | net_name_offset_unicode: Option, 493 | 494 | /// DeviceNameOffsetUnicode (4 bytes): An optional, 32-bit, unsigned 495 | /// integer that specifies the location of the DeviceNameUnicode field. 496 | /// This value is an offset, in bytes, from the start of the 497 | /// CommonNetworkRelativeLink structure. This field MUST be present if the 498 | /// value of the NetNameOffset field is greater than 0x00000014; otherwise, 499 | /// this field MUST NOT be present. 500 | #[br(if(net_name_offset > 0x00000014))] 501 | device_name_offset_unicode: Option, 502 | 503 | /// A NULL–terminated string, as defined by the system default code 504 | /// page, which specifies a server share path; for example, 505 | /// "\\server\share". 506 | #[br( 507 | args(StringEncoding::CodePage(default_codepage)), 508 | map=|n: NullTerminatedString| n.to_string() 509 | )] 510 | net_name: String, 511 | 512 | /// A NULL–terminated string, as defined by the system default code 513 | /// page, which specifies a device; for example, the drive letter 514 | /// "D:". 515 | #[br( 516 | if(flags.has_valid_device()), 517 | args(StringEncoding::CodePage(default_codepage)), 518 | map=|n: Option| n.map(|s| s.to_string()) 519 | )] 520 | device_name: Option, 521 | 522 | /// An optional, NULL–terminated, Unicode string that is the Unicode 523 | /// version of the NetName string. This field MUST be present if the value 524 | /// of the NetNameOffset field is greater than 0x00000014; otherwise, this 525 | /// field MUST NOT be present. 526 | #[br( 527 | if(net_name_offset > 0x00000014), 528 | args(StringEncoding::Unicode), 529 | map=|n: Option| n.map(|s| s.to_string()) 530 | )] 531 | net_name_unicode: Option, 532 | 533 | /// An optional, NULL–terminated, Unicode string that is the Unicode 534 | /// version of the DeviceName string. This field MUST be present if the 535 | /// value of the NetNameOffset field is greater than 0x00000014; otherwise, 536 | /// this field MUST NOT be present. 537 | #[br( 538 | if(net_name_offset > 0x00000014 && flags.has_valid_device()), 539 | args(StringEncoding::Unicode), 540 | map=|n: Option| n.map(|s| s.to_string()) 541 | )] 542 | device_name_unicode: Option, 543 | } 544 | 545 | impl From for Vec { 546 | fn from(_val: CommonNetworkRelativeLink) -> Self { 547 | unimplemented!() 548 | } 549 | } 550 | 551 | impl CommonNetworkRelativeLink { 552 | /// returns the name of this link 553 | pub fn name(&self) -> String { 554 | if self.flags.has_valid_device() { 555 | self.device_name_unicode 556 | .as_ref() 557 | .or(self.device_name.as_ref()) 558 | .expect("device name must be set, if VALID_DEVICE is set") 559 | .to_string() 560 | } else if self.flags.has_valid_net_type() { 561 | self.net_name_unicode 562 | .as_ref() 563 | .unwrap_or(&self.net_name) 564 | .to_string() 565 | } else { 566 | panic!("either one of VALID_DEVICE or VALID_NET_TYPE must be set") 567 | } 568 | } 569 | } 570 | 571 | bitflags! { 572 | /// Flags that specify the contents of the DeviceNameOffset and NetProviderType fields. 573 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 574 | #[cfg_attr(feature = "serde", derive(Serialize))] 575 | pub struct CommonNetworkRelativeLinkFlags: u32 { 576 | /// If set, the DeviceNameOffset field contains an offset to the device 577 | /// name. If not set, the DeviceNameOffset field does not contain an 578 | /// offset to the device name, and its value MUST be zero. 579 | const VALID_DEVICE = 0b0000_0000_0000_0000_0000_0000_0000_0001; 580 | /// If set, the NetProviderType field contains the network provider 581 | /// type. If not set, the NetProviderType field does not contain the 582 | /// network provider type, and its value MUST be zero. 583 | const VALID_NET_TYPE = 0b0000_0000_0000_0000_0000_0000_0000_0010; 584 | } 585 | } 586 | 587 | binread_flags!(CommonNetworkRelativeLinkFlags, u32); 588 | 589 | #[allow(missing_docs)] 590 | impl CommonNetworkRelativeLinkFlags { 591 | pub fn has_valid_device(&self) -> bool { 592 | *self & Self::VALID_DEVICE == Self::VALID_DEVICE 593 | } 594 | 595 | pub fn has_valid_net_type(&self) -> bool { 596 | *self & Self::VALID_NET_TYPE == Self::VALID_NET_TYPE 597 | } 598 | } 599 | 600 | /// A 32-bit, unsigned integer that specifies the type of network provider. 601 | /// 602 | #[allow(missing_docs)] 603 | #[derive(Clone, Debug, BinRead)] 604 | #[cfg_attr(feature = "serde", derive(Serialize))] 605 | #[br(repr(u32))] 606 | pub enum NetworkProviderType { 607 | None = 0, 608 | MSNet = 0x00010000, 609 | Smb = 0x00020000, 610 | Netware = 0x00030000, 611 | Vines = 0x00040000, 612 | TenNet = 0x00050000, 613 | Locus = 0x00060000, 614 | SunPCNFS = 0x00070000, 615 | LanStep = 0x00080000, 616 | NineTiles = 0x00090000, 617 | Lantastic = 0x000A0000, 618 | As400 = 0x000B0000, 619 | FTPNFS = 0x000C0000, 620 | PathWorks = 0x000D0000, 621 | LifeNet = 0x000E0000, 622 | PowerLAN = 0x000F0000, 623 | BWNFS = 0x00100000, 624 | Cogent = 0x00110000, 625 | Farallon = 0x00120000, 626 | AppleTalk = 0x00130000, 627 | Intergraph = 0x00140000, 628 | SymfoNet = 0x00150000, 629 | ClearCase = 0x00160000, 630 | Frontier = 0x00170000, 631 | BMC = 0x00180000, 632 | DCE = 0x00190000, 633 | Avid = 0x1a0000, 634 | Docuspace = 0x1b0000, 635 | Mangosoft = 0x1c0000, 636 | Sernet = 0x1d0000, 637 | Riverfront1 = 0x1e0000, 638 | Riverfront2 = 0x1f0000, 639 | Decorb = 0x200000, 640 | Protstor = 0x210000, 641 | FjRedir = 0x220000, 642 | Distinct = 0x230000, 643 | Twins = 0x240000, 644 | Rdr2Sample = 0x250000, 645 | CSC = 0x260000, 646 | _3In1 = 0x270000, 647 | ExtendNet = 0x290000, 648 | Stac = 0x2a0000, 649 | Foxbat = 0x2b0000, 650 | Yahoo = 0x2c0000, 651 | Exifs = 0x2d0000, 652 | Dav = 0x2e0000, 653 | Knoware = 0x2f0000, 654 | ObjectDire = 0x300000, 655 | Masfax = 0x310000, 656 | HobNfs = 0x320000, 657 | Shiva = 0x330000, 658 | Ibmal = 0x340000, 659 | Lock = 0x350000, 660 | Termsrv = 0x360000, 661 | Srt = 0x370000, 662 | Quincy = 0x380000, 663 | Openafs = 0x390000, 664 | Avid1 = 0x3a0000, 665 | Dfs = 0x3b0000, 666 | Kwnp = 0x3c0000, 667 | Zenworks = 0x3d0000, 668 | Driveonweb = 0x3e0000, 669 | Vmware = 0x3f0000, 670 | Rsfx = 0x400000, 671 | Mfiles = 0x410000, 672 | MsNfs = 0x420000, 673 | Google = 0x430000, 674 | } 675 | -------------------------------------------------------------------------------- /src/linktarget.rs: -------------------------------------------------------------------------------- 1 | use binrw::BinRead; 2 | #[allow(unused)] 3 | use log::{debug, error, info, trace, warn}; 4 | 5 | #[cfg(feature = "serde")] 6 | use serde::Serialize; 7 | 8 | use crate::{generic_types::idlist::IdList, itemid::ItemID}; 9 | 10 | /// The LinkTargetIDList structure specifies the target of the link. The presence of this optional 11 | /// structure is specified by the HasLinkTargetIDList bit (LinkFlagssection 2.1.1) in the 12 | /// ShellLinkHeader(section2.1). 13 | #[derive(Clone, Debug, Default, BinRead)] 14 | #[cfg_attr(feature = "serde", derive(Serialize))] 15 | pub struct LinkTargetIdList { 16 | /// The size, in bytes, of the IDList field. 17 | pub size: u16, 18 | /// A stored IDList structure (section 2.2.1), which contains the item ID list. An IDList 19 | /// structure conforms to the following ABNF \[RFC5234\]: 20 | /// `IDLIST = *ITEMID TERMINALID` 21 | #[br(args(size))] 22 | id_list: IdList, 23 | } 24 | 25 | impl LinkTargetIdList { 26 | /// returns a reference to internal list of [`ItemID`] items 27 | pub fn id_list(&self) -> &Vec { 28 | self.id_list.item_id_list() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/stringdata.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use crate::{strings::*, LinkFlags}; 4 | use binrw::BinRead; 5 | #[cfg(feature = "binwrite")] 6 | use binrw::BinWrite; 7 | use encoding_rs::Encoding; 8 | use getset::{Getters, Setters}; 9 | use serde::Serialize; 10 | 11 | /// StringData refers to a set of structures that convey user interface and 12 | /// path identification information. The presence of these optional structures 13 | /// is controlled by LinkFlags (section 2.1.1) in the ShellLinkHeader 14 | /// (section 2.1). 15 | #[derive(BinRead, Default, Getters, Setters, Debug, Serialize)] 16 | #[cfg_attr(feature = "binwrite", derive(BinWrite))] 17 | #[getset(get = "pub", set = "pub")] 18 | #[brw(import(link_flags: LinkFlags, encoding: &'static Encoding))] 19 | pub struct StringData { 20 | /// NAME_STRING: An optional structure that specifies a description of the 21 | /// shortcut that is displayed to end users to identify the purpose of the 22 | /// shell link. This structure MUST be present if the HasName flag is set. 23 | #[brw(args(link_flags, LinkFlags::HAS_NAME, encoding))] 24 | #[br(parse_with = parse_sized_string)] 25 | #[cfg_attr(feature="binwrite", bw(write_with=write_sized_string))] 26 | name_string: Option, 27 | 28 | /// RELATIVE_PATH: An optional structure that specifies the location of the 29 | /// link target relative to the file that contains the shell link. When 30 | /// specified, this string SHOULD be used when resolving the link. This 31 | /// structure MUST be present if the HasRelativePath flag is set. 32 | #[brw(args(link_flags, LinkFlags::HAS_RELATIVE_PATH, encoding))] 33 | #[br(parse_with = parse_sized_string)] 34 | #[cfg_attr(feature="binwrite", bw(write_with=write_sized_string))] 35 | relative_path: Option, 36 | 37 | /// WORKING_DIR: An optional structure that specifies the file system path 38 | /// of the working directory to be used when activating the link target. 39 | /// This structure MUST be present if the HasWorkingDir flag is set. 40 | #[brw(args(link_flags, LinkFlags::HAS_WORKING_DIR, encoding))] 41 | #[br(parse_with = parse_sized_string)] 42 | #[cfg_attr(feature="binwrite", bw(write_with=write_sized_string))] 43 | working_dir: Option, 44 | 45 | /// COMMAND_LINE_ARGUMENTS: An optional structure that stores the 46 | /// command-line arguments that are specified when activating the link 47 | /// target. This structure MUST be present if the HasArguments flag is set. 48 | #[brw(args(link_flags, LinkFlags::HAS_ARGUMENTS, encoding))] 49 | #[br(parse_with = parse_sized_string)] 50 | #[cfg_attr(feature="binwrite", bw(write_with=write_sized_string))] 51 | command_line_arguments: Option, 52 | 53 | /// ICON_LOCATION: An optional structure that specifies the location of the 54 | /// icon to be used when displaying a shell link item in an icon view. This 55 | /// structure MUST be present if the HasIconLocation flag is set. 56 | #[brw(args(link_flags, LinkFlags::HAS_ICON_LOCATION, encoding))] 57 | #[br(parse_with = parse_sized_string)] 58 | #[cfg_attr(feature="binwrite", bw(write_with=write_sized_string))] 59 | icon_location: Option, 60 | } 61 | 62 | impl Display for StringData { 63 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 64 | let mut parts = Vec::new(); 65 | 66 | if let Some(name_string) = self.name_string().as_ref() { 67 | parts.push(format!("name-string={name_string}")); 68 | } 69 | if let Some(relative_path) = self.relative_path().as_ref() { 70 | parts.push(format!("relative-path={relative_path}")); 71 | } 72 | if let Some(working_dir) = self.working_dir().as_ref() { 73 | parts.push(format!("working-dir={working_dir}")); 74 | } 75 | if let Some(command_line_arguments) = self.name_string().as_ref() { 76 | parts.push(format!("cli-args={command_line_arguments}")); 77 | } 78 | if let Some(icon_location) = self.icon_location().as_ref() { 79 | parts.push(format!("icon-location={icon_location}")); 80 | } 81 | 82 | parts.join(",").fmt(f) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/strings/fixed_size_string.rs: -------------------------------------------------------------------------------- 1 | use binrw::BinRead; 2 | use core::fmt::Display; 3 | use encoding_rs::Encoding; 4 | use substring::Substring; 5 | 6 | /// represents a string that is stored in a buffer of a fixed size 7 | #[derive(Clone, Debug)] 8 | pub struct FixedSizeString(String); 9 | 10 | impl BinRead for FixedSizeString { 11 | type Args<'a> = (usize, &'static Encoding); 12 | fn read_options( 13 | reader: &mut R, 14 | _endian: binrw::Endian, 15 | args: Self::Args<'_>, 16 | ) -> binrw::BinResult { 17 | let count = args.0; 18 | let encoding = args.1; 19 | let mut buffer = vec![0; count]; 20 | reader.read_exact(&mut buffer)?; 21 | 22 | let (cow, _, had_errors) = encoding.decode(&buffer[..]); 23 | if had_errors { 24 | return Err(binrw::error::Error::AssertFail { 25 | pos: reader.stream_position()?, 26 | message: format!( 27 | "unable to decode String to {} from buffer {buffer:?}", 28 | encoding.name() 29 | ), 30 | }); 31 | } 32 | let mut res = cow.to_string(); 33 | if let Some(last_character) = res.find('\u{0000}') { 34 | res = res.substring(0, last_character).to_string(); 35 | } 36 | Ok(Self(res)) 37 | } 38 | } 39 | 40 | impl Display for FixedSizeString { 41 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 42 | self.0.fmt(f) 43 | } 44 | } 45 | 46 | impl AsRef for FixedSizeString { 47 | fn as_ref(&self) -> &str { 48 | &self.0 49 | } 50 | } 51 | 52 | impl FixedSizeString { 53 | /// returns `true` if the string is empty and `false` otherwise 54 | pub fn is_empty(&self) -> bool { 55 | self.0.is_empty() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/strings/mod.rs: -------------------------------------------------------------------------------- 1 | mod fixed_size_string; 2 | mod null_terminated_string; 3 | mod sized_string; 4 | mod string_encoding; 5 | 6 | pub use fixed_size_string::*; 7 | pub use null_terminated_string::*; 8 | pub use sized_string::*; 9 | pub use string_encoding::*; 10 | 11 | /// this type wraps [`encoding_rs::Encoding`] so that depending crates do not 12 | /// need to to depend on [`encoding_rs`] 13 | pub type Encoding = &'static encoding_rs::Encoding; 14 | 15 | macro_rules! support_encoding { 16 | ($encoding:ident) => { 17 | pub static $encoding: crate::strings::Encoding = encoding_rs::$encoding; 18 | }; 19 | } 20 | 21 | /// this module reexports all statics from `encoding_rs` 22 | pub mod encoding { 23 | #![allow(missing_docs)] 24 | support_encoding!(WINDOWS_1250); 25 | support_encoding!(WINDOWS_1251); 26 | support_encoding!(WINDOWS_1252); 27 | support_encoding!(WINDOWS_1253); 28 | support_encoding!(WINDOWS_1254); 29 | support_encoding!(WINDOWS_1255); 30 | support_encoding!(WINDOWS_1256); 31 | support_encoding!(WINDOWS_1257); 32 | support_encoding!(WINDOWS_1258); 33 | support_encoding!(WINDOWS_874); 34 | support_encoding!(UTF_16LE); 35 | } 36 | -------------------------------------------------------------------------------- /src/strings/null_terminated_string.rs: -------------------------------------------------------------------------------- 1 | use binrw::{BinRead, BinReaderExt, NullWideString}; 2 | use core::fmt::Display; 3 | 4 | use crate::StringEncoding; 5 | 6 | /// represents a string of unknown length which is NULL-terminated 7 | #[derive(Clone, Debug)] 8 | pub struct NullTerminatedString(String); 9 | 10 | impl BinRead for NullTerminatedString { 11 | type Args<'a> = (StringEncoding,); 12 | 13 | fn read_options( 14 | reader: &mut R, 15 | _endian: binrw::Endian, 16 | args: Self::Args<'_>, 17 | ) -> binrw::BinResult { 18 | match args.0 { 19 | StringEncoding::CodePage(default_codepage) => { 20 | let mut buffer = Vec::new(); 21 | loop { 22 | let c: u8 = reader.read_le()?; 23 | if c == 0 { 24 | break; 25 | } else { 26 | buffer.push(c); 27 | } 28 | } 29 | let (cow, _, had_errors) = default_codepage.decode(&buffer); 30 | if had_errors { 31 | return Err(binrw::error::Error::AssertFail { 32 | pos: reader.stream_position()?, 33 | message: format!( 34 | "unable to decode String to CP1252 from buffer {buffer:?}" 35 | ), 36 | }); 37 | } 38 | Ok(Self(cow.to_string())) 39 | } 40 | StringEncoding::Unicode => { 41 | let s: NullWideString = reader.read_le()?; 42 | Ok(Self(s.to_string())) 43 | } 44 | } 45 | } 46 | } 47 | 48 | impl Display for NullTerminatedString { 49 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 50 | self.0.fmt(f) 51 | } 52 | } 53 | 54 | impl AsRef for NullTerminatedString { 55 | fn as_ref(&self) -> &str { 56 | &self.0 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/strings/sized_string.rs: -------------------------------------------------------------------------------- 1 | use binrw::{BinReaderExt, BinResult}; 2 | use encoding_rs::{Encoding, UTF_16LE}; 3 | use log::trace; 4 | 5 | #[cfg(feature = "binwrite")] 6 | use binrw::BinWrite; 7 | 8 | use crate::{LinkFlags, StringEncoding}; 9 | 10 | /// reads a sized string from `reader` and converts it into a [`String`] 11 | #[binrw::parser(reader: reader)] 12 | pub fn parse_sized_string( 13 | link_flags: LinkFlags, 14 | expected_flag: LinkFlags, 15 | encoding: &'static Encoding, 16 | ) -> BinResult> { 17 | if link_flags.contains(expected_flag) { 18 | log::info!("reading string at {}", reader.stream_position()?); 19 | let count_characters: u16 = reader.read_le()?; 20 | trace!( 21 | "reading sized string of size '{count_characters}' at 0x{:08x}", 22 | reader.stream_position()? 23 | ); 24 | 25 | let encoding = StringEncoding::from(link_flags, encoding); 26 | 27 | log::info!("characters: {count_characters}"); 28 | 29 | match encoding { 30 | StringEncoding::CodePage(default_encoding) => { 31 | let mut buffer = vec![0; count_characters.into()]; 32 | reader.read_exact(&mut buffer)?; 33 | let (cow, _, had_errors) = default_encoding.decode(&buffer); 34 | if had_errors { 35 | return Err(binrw::error::Error::AssertFail { 36 | pos: reader.stream_position()?, 37 | message: format!( 38 | "unable to decode String to CP1252 from buffer {buffer:?}" 39 | ), 40 | }); 41 | } 42 | Ok(Some(cow.to_string())) 43 | } 44 | StringEncoding::Unicode => { 45 | let mut buffer = vec![0; (count_characters * 2).into()]; 46 | reader.read_exact(&mut buffer)?; 47 | let (cow, _, had_errors) = UTF_16LE.decode(&buffer); 48 | if had_errors { 49 | return Err(binrw::error::Error::AssertFail { 50 | pos: reader.stream_position()?, 51 | message: format!( 52 | "unable to decode String to UTF-16LE from buffer {buffer:?}" 53 | ), 54 | }); 55 | } 56 | Ok(Some(cow.to_string())) 57 | } 58 | } 59 | } else { 60 | Ok(None) 61 | } 62 | } 63 | 64 | /// converts a [`String`] to a sized string and writes it 65 | #[cfg(feature = "binwrite")] 66 | #[cfg_attr(feature="binwrite", binrw::writer(writer: writer))] 67 | pub fn write_sized_string( 68 | s: &Option, 69 | link_flags: LinkFlags, 70 | expected_flag: LinkFlags, 71 | encoding: &'static Encoding, 72 | ) -> BinResult<()> { 73 | if link_flags.contains(expected_flag) { 74 | assert!(s.is_some()); 75 | let s = s 76 | .as_ref() 77 | .expect("the flags indicate that there should be a value, but there is none"); 78 | let count_characters = u16::try_from(s.len()).map_err(|_| binrw::Error::Custom { 79 | pos: writer.stream_position().unwrap(), 80 | err: Box::new("String is too long to be written"), 81 | })?; 82 | count_characters.write_le(writer)?; 83 | 84 | let encoding = StringEncoding::from(link_flags, encoding); 85 | match encoding { 86 | StringEncoding::CodePage(cp) => cp.encode(s).0.write(writer)?, 87 | StringEncoding::Unicode => { 88 | let v: Vec<_> = s.encode_utf16().collect(); 89 | v.write_le(writer)? 90 | } 91 | }; 92 | Ok(()) 93 | } else { 94 | assert!(s.is_none()); 95 | Ok(()) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/strings/string_encoding.rs: -------------------------------------------------------------------------------- 1 | use encoding_rs::UTF_16LE; 2 | 3 | use crate::LinkFlags; 4 | 5 | /// enum to select which string encoding should be used 6 | #[derive(Copy, Clone, Debug)] 7 | pub enum StringEncoding { 8 | /// use the system default code page 9 | CodePage(crate::strings::Encoding), 10 | 11 | /// use UNICODE (which is UTF-16LE on Windows) 12 | Unicode, 13 | } 14 | 15 | impl StringEncoding { 16 | /// creates string encoding information based on the given [`LinkFlags`] 17 | /// and the default encoding 18 | pub fn from(link_flags: LinkFlags, default_codepage: crate::strings::Encoding) -> Self { 19 | if link_flags.contains(LinkFlags::IS_UNICODE) { 20 | Self::Unicode 21 | } else { 22 | Self::CodePage(default_codepage) 23 | } 24 | } 25 | 26 | /// returns the effective encoding 27 | pub fn encoding(&self) -> crate::strings::Encoding { 28 | match self { 29 | StringEncoding::CodePage(cp) => cp, 30 | StringEncoding::Unicode => UTF_16LE, 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/create-read-blank.rs: -------------------------------------------------------------------------------- 1 | use lnk::{encoding::WINDOWS_1252, StringEncoding}; 2 | use log::info; 3 | 4 | use std::fs; 5 | 6 | const TEST_FILE_NAME: &'static str = "temp.lnk"; 7 | 8 | #[test] 9 | fn create_read_blank() { 10 | pretty_env_logger::init(); 11 | 12 | for encoding in &[ 13 | StringEncoding::Unicode, 14 | StringEncoding::CodePage(WINDOWS_1252), 15 | ] { 16 | info!("Saving shortcut..."); 17 | let mut shortcut = lnk::ShellLink::default().with_encoding(encoding); 18 | shortcut.set_name(Some("Blank name".to_string())); 19 | shortcut 20 | .save(TEST_FILE_NAME) 21 | .expect("Failed to save shortcut!"); 22 | 23 | info!("Reading shortcut..."); 24 | 25 | let shortcut = lnk::ShellLink::open(TEST_FILE_NAME, encoding.encoding()).unwrap(); 26 | //println!("{:#?}", shortcut); 27 | assert_eq!( 28 | shortcut.string_data().name_string(), 29 | &Some("Blank name".to_string()) 30 | ); 31 | } 32 | 33 | info!("Cleaning up..."); 34 | fs::remove_file(TEST_FILE_NAME).expect("delete shortcut"); 35 | } 36 | -------------------------------------------------------------------------------- /tests/data/Hearthstone.lnk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilopkins/lnk-rs/7087c251f026b8f9a9b88a8363eb06cb67d230d5/tests/data/Hearthstone.lnk -------------------------------------------------------------------------------- /tests/data/Windows PowerShell.lnk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilopkins/lnk-rs/7087c251f026b8f9a9b88a8363eb06cb67d230d5/tests/data/Windows PowerShell.lnk -------------------------------------------------------------------------------- /tests/data/blank.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilopkins/lnk-rs/7087c251f026b8f9a9b88a8363eb06cb67d230d5/tests/data/blank.txt -------------------------------------------------------------------------------- /tests/data/hotkey-no-problem.lnk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilopkins/lnk-rs/7087c251f026b8f9a9b88a8363eb06cb67d230d5/tests/data/hotkey-no-problem.lnk -------------------------------------------------------------------------------- /tests/data/hotkey-problem.lnk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilopkins/lnk-rs/7087c251f026b8f9a9b88a8363eb06cb67d230d5/tests/data/hotkey-problem.lnk -------------------------------------------------------------------------------- /tests/data/iron-heart.exe - Shortcut.lnk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilopkins/lnk-rs/7087c251f026b8f9a9b88a8363eb06cb67d230d5/tests/data/iron-heart.exe - Shortcut.lnk -------------------------------------------------------------------------------- /tests/data/iron-heart.exe - non-latin Shortcut.lnk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilopkins/lnk-rs/7087c251f026b8f9a9b88a8363eb06cb67d230d5/tests/data/iron-heart.exe - non-latin Shortcut.lnk -------------------------------------------------------------------------------- /tests/data/test.lnk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilopkins/lnk-rs/7087c251f026b8f9a9b88a8363eb06cb67d230d5/tests/data/test.lnk -------------------------------------------------------------------------------- /tests/hotkey-key-problem.rs: -------------------------------------------------------------------------------- 1 | use lnk::{encoding::WINDOWS_1252, HotkeyKey, HotkeyModifiers, ShellLink}; 2 | 3 | ///https://github.com/lilopkins/lnk-rs/pull/27 4 | 5 | #[test] 6 | fn tes_failing_hotkey_key_shortcut() { 7 | let _ = pretty_env_logger::try_init(); 8 | 9 | let shortcut = ShellLink::open("tests/data/hotkey-problem.lnk", WINDOWS_1252).unwrap(); 10 | assert_eq!(*shortcut.header().hotkey().key(), HotkeyKey::NoKeyAssigned); 11 | assert_eq!( 12 | *shortcut.header().hotkey().modifiers(), 13 | HotkeyModifiers::NO_MODIFIER 14 | ); 15 | } 16 | 17 | #[test] 18 | fn tes_no_failing_hotkey_key_shortcut() { 19 | let _ = pretty_env_logger::try_init(); 20 | 21 | let shortcut = ShellLink::open("tests/data/hotkey-no-problem.lnk", WINDOWS_1252).unwrap(); 22 | assert_eq!(*shortcut.header().hotkey().key(), HotkeyKey::Key1); 23 | assert_eq!( 24 | *shortcut.header().hotkey().modifiers(), 25 | HotkeyModifiers::HOTKEYF_ALT | HotkeyModifiers::HOTKEYF_CONTROL 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /tests/issuecomment-2560550817-failing-shortcut.rs: -------------------------------------------------------------------------------- 1 | use lnk::{encoding::WINDOWS_1252, ShellLink}; 2 | use log::debug; 3 | 4 | ///https://github.com/lilopkins/lnk-rs/pull/21#issuecomment-2560550817 5 | 6 | #[test] 7 | fn tes_failing_shortcut() { 8 | let _ = pretty_env_logger::try_init(); 9 | 10 | let shortcut = 11 | ShellLink::open("tests/data/iron-heart.exe - Shortcut.lnk", WINDOWS_1252).unwrap(); 12 | debug!("{:#?}", shortcut); 13 | } 14 | 15 | #[test] 16 | fn test_non_latin_shortcut() { 17 | let _ = pretty_env_logger::try_init(); 18 | 19 | let shortcut = ShellLink::open( 20 | "tests/data/iron-heart.exe - non-latin Shortcut.lnk", 21 | WINDOWS_1252, 22 | ) 23 | .unwrap(); 24 | debug!("{:#?}", shortcut); 25 | } 26 | -------------------------------------------------------------------------------- /tests/test.rs: -------------------------------------------------------------------------------- 1 | const TEST_FILE_NAME: &str = "tests/data/test.lnk"; 2 | const TEST_BLANK_FILE_NAME: &str = "tests/data/blank.txt"; 3 | 4 | use chrono::NaiveDate; 5 | use encoding::WINDOWS_1252; 6 | use lnk::*; 7 | #[allow(unused)] 8 | use log::{debug, error, info, trace, warn}; 9 | 10 | #[test] 11 | fn test_lnk_header() { 12 | let _ = pretty_env_logger::try_init(); 13 | 14 | let shortcut = ShellLink::open(TEST_FILE_NAME, WINDOWS_1252).unwrap(); 15 | debug!("{:#?}", shortcut); 16 | 17 | assert_eq!( 18 | *shortcut.header().link_flags(), 19 | LinkFlags::HAS_LINK_TARGET_ID_LIST 20 | | LinkFlags::HAS_LINK_INFO 21 | | LinkFlags::HAS_RELATIVE_PATH 22 | | LinkFlags::HAS_WORKING_DIR 23 | | LinkFlags::IS_UNICODE 24 | | LinkFlags::ENABLE_TARGET_METADATA, 25 | "Link flags should be parsed correctly" 26 | ); 27 | 28 | assert_eq!( 29 | *shortcut.header().file_attributes(), 30 | FileAttributeFlags::FILE_ATTRIBUTE_ARCHIVE, 31 | "File attributes should be parsed correctly" 32 | ); 33 | 34 | assert_eq!( 35 | shortcut.header().creation_time().datetime().date(), 36 | NaiveDate::from_ymd_opt(2008, 9, 12).unwrap(), 37 | "Creation time should be parsed correctly" 38 | ); 39 | assert_eq!( 40 | shortcut.header().access_time().datetime().date(), 41 | NaiveDate::from_ymd_opt(2008, 9, 12).unwrap(), 42 | "Access time should be parsed correctly" 43 | ); 44 | assert_eq!( 45 | shortcut.header().write_time().datetime().date(), 46 | NaiveDate::from_ymd_opt(2008, 9, 12).unwrap(), 47 | "Write time should be parsed correctly" 48 | ); 49 | 50 | assert_eq!( 51 | *shortcut.header().file_size(), 52 | 0x00, 53 | "File size should be parsed correctly" 54 | ); 55 | assert_eq!( 56 | *shortcut.header().icon_index(), 57 | 0x00, 58 | "Icon index should be parsed correctly" 59 | ); 60 | assert_eq!( 61 | *shortcut.header().show_command(), 62 | ShowCommand::ShowNormal, 63 | "Show command should be parsed correctly" 64 | ); 65 | assert_eq!(*shortcut.header().hotkey().key(), HotkeyKey::NoKeyAssigned); 66 | assert_eq!( 67 | *shortcut.header().hotkey().modifiers(), 68 | HotkeyModifiers::NO_MODIFIER 69 | ); 70 | 71 | assert_eq!(shortcut.string_data().name_string(), &None); 72 | assert_eq!( 73 | shortcut.string_data().relative_path(), 74 | &Some(r".\a.txt".to_string()) 75 | ); 76 | assert_eq!( 77 | shortcut.string_data().working_dir(), 78 | &Some(r"C:\test".to_string()) 79 | ); 80 | } 81 | 82 | #[test] 83 | fn test_no_panic_reading_other_filetypes() { 84 | let _ = pretty_env_logger::try_init(); 85 | 86 | let res = ShellLink::open(TEST_BLANK_FILE_NAME, WINDOWS_1252); 87 | // Shouldn't have panicked by now! 88 | assert!(res.is_err()); 89 | } 90 | --------------------------------------------------------------------------------