using System; using System.IO; using System.Runtime.InteropServices.ComTypes; using System.Security.AccessControl; using System.Security.Permissions; using Vanara.Extensions; using Vanara.InteropServices; using Vanara.PInvoke; using static Vanara.PInvoke.Kernel32; using static Vanara.PInvoke.Macros; using static Vanara.PInvoke.Ole32; using static Vanara.PInvoke.Shell32; using static Vanara.PInvoke.User32; namespace Vanara.Windows.Shell { /// Flags determining how the links with missing targets are resolved. [Flags] public enum LinkResolution : uint { /// No flags set. None = 0, /// /// Do not display a dialog box if the link cannot be resolved. When NoUI is set, a time-out value that specifies the maximum amount /// of time to be spent resolving the link can be specified in milliseconds. The function returns if the link cannot be resolved /// within the time-out duration. If the timeout is not set, the time-out duration will be set to the default value of 3,000 /// milliseconds (3 seconds). /// NoUI = 0x1, /// Allow any match during resolution. Has no effect on ME/2000 or above, use the other flags instead. AnyMatch = 0x2, /// /// If the link object has changed, update its path and list of identifiers. If UPDATE is set, you do not need to call /// IPersistFile::IsDirty to determine whether or not the link object has changed. /// Update = 0x4, /// Do not update the link information. NoUpdate = 0x8, /// Do not execute the search heuristics. NoSearch = 0x10, /// Do not use distributed link tracking. NoTrack = 0x20, /// /// Disable distributed link tracking. By default, distributed link tracking tracks removable media across multiple devices based on /// the volume name. It also uses the UNC path to track remote file systems whose drive letter has changed. Setting NoLinkInfo /// disables both types of tracking. /// NoLinkInfo = 0x40, /// Call the Microsoft Windows Installer. InvokeMSI = 0x80, /// Windows XP and later. Assume same as NoUI but intended for applications without a hWnd. NoUIWithMsgPump = 0x101, /// /// Windows 7 and later. Offer the option to delete the shortcut when this method is unable to resolve it, even if the shortcut is /// not a shortcut to a file. /// OfferDeleteWithoutFile = 0x200, /// /// Windows 7 and later. Report as dirty if the target is a known folder and the known folder was redirected. This only works if the /// original target path was a file system path or ID list and not an aliased known folder ID list. /// KnownFolder = 0x400, /// /// Windows 7 and later. Resolve the computer name in UNC targets that point to a local computer. This value is used with SLDFKEEPLOCALIDLISTFORUNCTARGET. /// MachineInLocalTarget = 0x800, /// Windows 7 and later. Update the computer GUID and user SID if necessary. UpdateMachineAndSid = 0x1000, /// ?? Assuming this does not update the Object ID NoObjectID = 0x2000 } /// Represents a Shell Shortcut (.lnk) file. public sealed class ShellLink : ShellItem, IEquatable, IEquatable { internal IShellLinkW link; /// Initializes a new instance of the class, which acts as a wrapper for a .lnk file. /// The shortcut file (.lnk) to load. /// /// The window that the Shell will use as the parent for a dialog box. The Shell displays the dialog box if it needs to prompt the /// user for more information while resolving a Shell link. /// /// The resolve flags. /// The time out. /// linkFile public ShellLink(string linkFile, LinkResolution resolveFlags = LinkResolution.NoUI, HWND window = default, TimeSpan timeOut = default) : base(linkFile) => LoadAndResolve(linkFile, (SLR_FLAGS)resolveFlags, window, (ushort)timeOut.TotalMilliseconds); /// /// Initializes a new instance of the class and sets many properties. This link is not saved as a file. /// /// The full path to the target file. /// The arguments for the target's execution. /// The working directory for the execution of the target. /// The description of the link. public ShellLink(string targetFilename, string arguments, string workingDirectory = null, string description = null) : this() { TargetPath = targetFilename; Description = description; WorkingDirectory = workingDirectory; Arguments = arguments; // TODO: Determine if IShellItem is required. => Init(null); } internal ShellLink(IShellItem iItem) => LoadAndResolve(iItem.GetDisplayName(SIGDN.SIGDN_FILESYSPATH), SLR_FLAGS.SLR_NO_UI); private ShellLink() => link = new IShellLinkW(); /// Gets/sets any command line arguments associated with the link public string Arguments { get => GetStringValue(link.GetArguments, MAX_PATH); set { link.SetArguments(value); Save(); } } /// Gets the current option settings. /// One or more of the SHELL_LINK_DATA_FLAGS that indicate the current option settings. public SHELL_LINK_DATA_FLAGS DataFlags { get => ((IShellLinkDataList)link).GetFlags(); set { ((IShellLinkDataList)link).SetFlags(value); Save(); } } /// Gets/sets the description of the link public string Description { get => GetStringValue(link.GetDescription, ComCtl32.INFOTIPSIZE); set { link.SetDescription(value); Save(); } } /// Gets/sets the HotKey to start the shortcut (if any). /// /// /// The keyboard shortcut. The virtual key code is in the low-order byte, and the modifier flags are in the high-order byte. The /// modifier flags can be a combination of the following values. /// /// /// /// Value /// Meaning /// /// /// HOTKEYF_ALT 0x04 /// ALT key /// /// /// HOTKEYF_CONTROL 0x02 /// CTRL key /// /// /// HOTKEYF_EXT 0x08 /// Extended key /// /// /// HOTKEYF_SHIFT 0x01 /// SHIFT key /// /// /// public ushort HotKey { get => link.GetHotKey(); set { link.SetHotKey(value); Save(); } } /// Gets the index of this icon within the icon path's resources. public IconLocation IconLocation { get { var iconIndex = 0; try { return new IconLocation(GetStringValue((sb, l) => link.GetIconLocation(sb, l, out iconIndex)), iconIndex); } catch { return null; } } set { link.SetIconLocation(value.ModuleFileName, value.ResourceId); Save(); } } /// Get or sets the list of item identifiers for a Shell link. public PIDL IDList { get => link.GetIDList(); set { link.SetIDList(value); Save(); } } /// Gets a value indicating whether this instance is link. /// true if this instance is link; otherwise, false. public override bool IsLink => true; /// Gets/sets the relative path to the link's target public string RelativeTargetPath { get => GetPath(SLGP.SLGP_RELATIVEPRIORITY); set { link.SetRelativePath(value); Save(); } } /// Gets or sets a value indicating whether the target is run with Administrator rights. /// true if run as Administrator; otherwise, false. public bool RunAsAdministrator { get => DataFlags.IsFlagSet(SHELL_LINK_DATA_FLAGS.SLDF_RUNAS_USER); set { var pdl = (IShellLinkDataList)link; pdl.SetFlags(pdl.GetFlags().SetFlags(SHELL_LINK_DATA_FLAGS.SLDF_RUNAS_USER, value)); Save(); } } /// Gets/sets the short (8.3 format) path to the link's target public string ShortTargetPath => GetPath(SLGP.SLGP_SHORTPATH); /// Gets or sets the show command for a Shell link object. /// The show command for a Shell link object. public ShowWindowCommand ShowState { get => link.GetShowCmd() - 1; set { link.SetShowCmd(value + 1); Save(); } } /// Gets or sets the target with a instance. public ShellItem Target { get => TargetPath is null ? null : new ShellItem(TargetPath); set { link.SetIDList(value.PIDL); Save(); } } /// Gets/sets the fully qualified path to the link's target public string TargetPath { get => GetPath(SLGP.SLGP_RAWPATH); set { link.SetPath(value); Save(); } } /// Gets/sets the display name of the link through the PKEY_Title property. public string Title { get => (string)Properties[PROPERTYKEY.System.Title]; set { Properties[PROPERTYKEY.System.Title] = value; Properties.Commit(); Save(); } } /// Gets/sets the Working Directory for the Link public string WorkingDirectory { get => GetStringValue(link.GetWorkingDirectory, MAX_PATH); set { link.SetWorkingDirectory(value); Save(); } } /// Creates or overwrites a new link file. /// The link filename. /// The full path to the target file. /// The description of the link. /// The working directory for the execution of the target. /// The arguments for the target's execution. /// An instance of a representing the values supplied. public static ShellLink Create(string linkFilename, string targetFilename, string description = null, string workingDirectory = null, string arguments = null) { if (File.Exists(linkFilename)) throw new InvalidOperationException("File already exists."); var lnk = new ShellLink(targetFilename, arguments, workingDirectory, description); lnk.SaveAs(linkFilename); return lnk; } /// Creates or overwrites a new link file. /// The link filename. /// The ShellItem for the target. /// The description of the link. /// The working directory for the execution of the target. /// The arguments for the target's execution. /// An instance of a representing the values supplied. public static ShellLink Create(string linkFilename, ShellItem target, string description = null, string workingDirectory = null, string arguments = null) { if (File.Exists(linkFilename)) throw new InvalidOperationException("File already exists."); var lnk = new ShellLink { link = new IShellLinkW(), Target = target, Description = description, WorkingDirectory = workingDirectory, Arguments = arguments }; lnk.SaveAs(linkFilename); lnk.Init(SHCreateItemFromParsingName(Path.GetFullPath(linkFilename))); return lnk; } /// Copies an existing file to a new file, allowing the overwriting of an existing file. /// The name of the new file to copy to. /// true to allow an existing file to be overwritten; otherwise false. /// /// A new file, or an overwrite of an existing file if overwrite is true. If the file exists and overwrite is false, an IOException /// is thrown. /// public ShellLink CopyTo(string destShellLink, bool overwrite = false) { File.Copy(FileSystemPath, destShellLink, overwrite); return new ShellLink(destShellLink); } /// Dispose the object, releasing the COM ShellLink object public override void Dispose() { link = null; //Release(link); base.Dispose(); } /// Determines whether the specified , is equal to this instance. /// The to compare with this instance. /// if the specified is equal to this instance; otherwise, . public override bool Equals(object obj) => obj switch { ShellLink sl => Equals(sl), IShellLinkW isl => Equals(isl), _ => base.Equals(obj), }; /// /// Gets a FileSecurity object that encapsulates the specified type of access control list (ACL) entries for the file described by /// the current FileInfo object. /// /// /// One of the AccessControlSections values that specifies which group of access control entries to retrieve. /// /// A FileSecurity object that encapsulates the access control rules for the current file. public FileSecurity GetAccessControl(AccessControlSections includeSections = AccessControlSections.Access | AccessControlSections.Group | AccessControlSections.Owner) => new(FileSystemPath, includeSections); /// Returns a hash code for this instance. /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. public override int GetHashCode() => ToString().GetHashCode(); /// Gets the icon for this link file. /// if set to true retrieve the large icon; other retrieve the small icon. /// The icon. public SafeHICON GetIcon(bool large) { var loc = IconLocation; if (loc.IsValid) return loc.Icon; // If there are no details set for the icon, then we must use the shell to get the icon for the target var sfi = new ShellFileInfo(TargetPath); return large ? sfi.LargeIcon : sfi.SmallIcon; } /// /// Applies access control list (ACL) entries described by a FileSecurity object to the file described by the current FileInfo object. /// /// /// A FileSecurity object that describes an access control list (ACL) entry to apply to the current file. /// public void SetAccessControl(FileSecurity fileSecurity) => new FileInfo(FileSystemPath).SetAccessControl(fileSecurity); /// Returns a that represents this instance. /// A that represents this instance. // Path and title should be case insensitive. Shell treats arguments as case sensitive because apps can handle those differently. public override string ToString() => $"{Title?.ToUpperInvariant() ?? ""} {TargetPath.ToUpperInvariant()} {Arguments}"; private string GetPath(SLGP value) => GetStringValue((sb, l) => link.GetPath(sb, l, out _, value)); private void InitBaseFromPath(string linkFilename) => Init(SHCreateItemFromParsingName(Path.GetFullPath(linkFilename))); private void LoadAndResolve(string linkFile, SLR_FLAGS resolveFlags, HWND hWin = default, ushort timeOut = 0) { var fullPath = Path.GetFullPath(linkFile ?? throw new ArgumentNullException(nameof(linkFile))); if (!File.Exists(fullPath)) throw new FileNotFoundException("Link file not found.", linkFile); link = new IShellLinkW(); if (resolveFlags.IsFlagSet(SLR_FLAGS.SLR_NO_UI) && timeOut != 0) resolveFlags = (SLR_FLAGS)MAKELONG((ushort)resolveFlags, timeOut); #if !NETSTANDARD new FileIOPermission(FileIOPermissionAccess.Read, fullPath).Demand(); #endif ((IPersistFile)link).Load(fullPath, (int)STGM.STGM_DIRECT); link.Resolve(hWin, resolveFlags); InitBaseFromPath(linkFile); } /// Saves the shortcut to ShortCutFile. private void Save() { if (string.IsNullOrEmpty(FileSystemPath)) return; SaveAs(File.Exists(FileSystemPath) ? null : FileSystemPath); } /// Saves the shortcut to the specified file. /// The shortcut file (.lnk). public void SaveAs(string linkFile) { using (var pIPF = ComReleaserFactory.Create((IPersistFile)link)) pIPF.Item.Save(linkFile, true); if (linkFile != null) InitBaseFromPath(linkFile); } /// Determines if two shell links are equal by looking at the title, path and arguments. /// The left shell link to evaluate. /// The right shell link to evaluate. /// if the two links are equal; otherwise . internal static bool Equals(IShellLinkW left, IShellLinkW right) { if (ReferenceEquals(left, right)) return true; if (left is null || right is null) return false; return string.Equals(GetCompareString(left), GetCompareString(right), StringComparison.Ordinal); } /// Gets a string representation of an instance that includes the title, path and arguments for comparison. /// The instance. /// The string representation for comparison. internal static string GetCompareString(IShellLinkW l) { var ps = (PropSys.IPropertyStore)l; var title = ps.GetValue(PROPERTYKEY.System.Title)?.ToString(); var exe = GetStringValue((sb, z) => l.GetPath(sb, z, out _, SLGP.SLGP_RAWPATH)); var args = GetStringValue(l.GetArguments, MAX_PATH); return exe?.ToUpperInvariant() + title?.ToUpperInvariant() + args; } /// Determines whether the specified , is equal to this instance. /// The to compare with this instance. /// if the specified is equal to this instance; otherwise, . public bool Equals(IShellLinkW other) => Equals(link, other); /// Determines whether the specified , is equal to this instance. /// The to compare with this instance. /// if the specified is equal to this instance; otherwise, . public bool Equals(ShellLink other) => Equals(link, other?.link); } }