English 中文(简体)
Java中的Windows快捷方式(.lnk)解析器?
原标题:
  • 时间:2008-11-21 17:09:05
  •  标签:

我目前在Java中使用Win32ShellFolderManager2ShellFolder.getLinkLocation来解析Windows快捷方式。不幸的是,如果Java程序在Vista下作为服务运行,则getLinkLocation将无法工作。具体而言,我会收到一个异常,指出“无法获取Shell文件夹ID列表”。

在网络上搜索确实可以找到有关此错误消息的提及,但始终与JFileChooser相关。我没有使用JFileChooser,我只需要将.lnk文件解析到其目标即可。

有人知道我可以使用的Java编写的第三方解析器来解析.lnk文件吗?

我已经找到了 .lnk 格式的非官方文档(这里),但是如果有人已经完成了这个工作,我宁愿不自己去做,因为这种格式十分复杂。

最佳回答

添加了注释(一些解释以及目前每个贡献者的贡献),对文件的魔数进行了额外检查,快速测试以查看给定的文件是否可能是有效的链接(而无需读取所有字节),修复了一个问题,如果文件太小,则抛出ParseException和适当的消息,进行了一些常规清理。

源码在这里(如果您有任何更改,请直接推到 GitHub repo/项目中)。

package org.stackoverflowusers.file;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;

/**
 * Represents a Windows shortcut (typically visible to Java only as a  .lnk  file).
 *
 * Retrieved 2011-09-23 from http://stackoverflow.com/questions/309495/windows-shortcut-lnk-parser-in-java/672775#672775
 * Originally called LnkParser
 *
 * Written by: (the stack overflow users, obviously!)
 *   Apache Commons VFS dependency removed by crysxd (why were we using that!?) https://github.com/crysxd
 *   Headerified, refactored and commented by Code Bling http://stackoverflow.com/users/675721/code-bling
 *   Network file support added by Stefan Cordes http://stackoverflow.com/users/81330/stefan-cordes
 *   Adapted by Sam Brightman http://stackoverflow.com/users/2492/sam-brightman
 *   Based on information in  The Windows Shortcut File Format  by Jesse Hager <jessehager@iname.com>
 *   And somewhat based on code from the book  Swing Hacks: Tips and Tools for Killer GUIs 
 *     by Joshua Marinacci and Chris Adamson
 *     ISBN: 0-596-00907-0
 *     http://www.oreilly.com/catalog/swinghks/
 */
public class WindowsShortcut
{
    private boolean isDirectory;
    private boolean isLocal;
    private String real_file;

    /**
     * Provides a quick test to see if this could be a valid link !
     * If you try to instantiate a new WindowShortcut and the link is not valid,
     * Exceptions may be thrown and Exceptions are extremely slow to generate,
     * therefore any code needing to loop through several files should first check this.
     *
     * @param file the potential link
     * @return true if may be a link, false otherwise
     * @throws IOException if an IOException is thrown while reading from the file
     */
    public static boolean isPotentialValidLink(File file) throws IOException {
        final int minimum_length = 0x64;
        InputStream fis = new FileInputStream(file);
        boolean isPotentiallyValid = false;
        try {
            isPotentiallyValid = file.isFile()
                && file.getName().toLowerCase().endsWith(".lnk")
                && fis.available() >= minimum_length
                && isMagicPresent(getBytes(fis, 32));
        } finally {
            fis.close();
        }
        return isPotentiallyValid;
    }

    public WindowsShortcut(File file) throws IOException, ParseException {
        InputStream in = new FileInputStream(file);
        try {
            parseLink(getBytes(in));
        } finally {
            in.close();
        }
    }

    /**
     * @return the name of the filesystem object pointed to by this shortcut
     */
    public String getRealFilename() {
        return real_file;
    }

    /**
     * Tests if the shortcut points to a local resource.
     * @return true if the  local  bit is set in this shortcut, false otherwise
     */
    public boolean isLocal() {
        return isLocal;
    }

    /**
     * Tests if the shortcut points to a directory.
     * @return true if the  directory  bit is set in this shortcut, false otherwise
     */
    public boolean isDirectory() {
        return isDirectory;
    }

    /**
     * Gets all the bytes from an InputStream
     * @param in the InputStream from which to read bytes
     * @return array of all the bytes contained in  in 
     * @throws IOException if an IOException is encountered while reading the data from the InputStream
     */
    private static byte[] getBytes(InputStream in) throws IOException {
        return getBytes(in, null);
    }
    
    /**
     * Gets up to max bytes from an InputStream
     * @param in the InputStream from which to read bytes
     * @param max maximum number of bytes to read
     * @return array of all the bytes contained in  in 
     * @throws IOException if an IOException is encountered while reading the data from the InputStream
     */
    private static byte[] getBytes(InputStream in, Integer max) throws IOException {
        // read the entire file into a byte buffer
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        byte[] buff = new byte[256];
        while (max == null || max > 0) {
            int n = in.read(buff);
            if (n == -1) {
                break;
            }
            bout.write(buff, 0, n);
            if (max != null)
                max -= n;
        }
        in.close();
        return bout.toByteArray();
    }

    private static boolean isMagicPresent(byte[] link) {
        final int magic = 0x0000004C;
        final int magic_offset = 0x00;
        return link.length >= 32 && bytesToDword(link, magic_offset) == magic;
    }

    /**
     * Gobbles up link data by parsing it and storing info in member fields
     * @param link all the bytes from the .lnk file
     */
    private void parseLink(byte[] link) throws ParseException {
        try {
            if (!isMagicPresent(link))
                throw new ParseException("Invalid shortcut; magic is missing", 0);

            // get the flags byte
            byte flags = link[0x14];

            // get the file attributes byte
            final int file_atts_offset = 0x18;
            byte file_atts = link[file_atts_offset];
            byte is_dir_mask = (byte)0x10;
            if ((file_atts & is_dir_mask) > 0) {
                isDirectory = true;
            } else {
                isDirectory = false;
            }

            // if the shell settings are present, skip them
            final int shell_offset = 0x4c;
            final byte has_shell_mask = (byte)0x01;
            int shell_len = 0;
            if ((flags & has_shell_mask) > 0) {
                // the plus 2 accounts for the length marker itself
                shell_len = bytesToWord(link, shell_offset) + 2;
            }

            // get to the file settings
            int file_start = 0x4c + shell_len;

            final int file_location_info_flag_offset_offset = 0x08;
            int file_location_info_flag = link[file_start + file_location_info_flag_offset_offset];
            isLocal = (file_location_info_flag & 2) == 0;
            // get the local volume and local system values
            //final int localVolumeTable_offset_offset = 0x0C;
            final int basename_offset_offset = 0x10;
            final int networkVolumeTable_offset_offset = 0x14;
            final int finalname_offset_offset = 0x18;
            int finalname_offset = link[file_start + finalname_offset_offset] + file_start;
            String finalname = getNullDelimitedString(link, finalname_offset);
            if (isLocal) {
                int basename_offset = link[file_start + basename_offset_offset] + file_start;
                String basename = getNullDelimitedString(link, basename_offset);
                real_file = basename + finalname;
            } else {
                int networkVolumeTable_offset = link[file_start + networkVolumeTable_offset_offset] + file_start;
                int shareName_offset_offset = 0x08;
                int shareName_offset = link[networkVolumeTable_offset + shareName_offset_offset]
                    + networkVolumeTable_offset;
                String shareName = getNullDelimitedString(link, shareName_offset);
                real_file = shareName + "\" + finalname;
            }
        } catch (ArrayIndexOutOfBoundsException e) {
            throw new ParseException("Could not be parsed, probably not a valid WindowsShortcut", 0);
        }
    }

    private static String getNullDelimitedString(byte[] bytes, int off) {
        int len = 0;
        // count bytes until the null character (0)
        while (true) {
            if (bytes[off + len] == 0) {
                break;
            }
            len++;
        }
        return new String(bytes, off, len);
    }

    /*
     * convert two bytes into a short note, this is little endian because it s
     * for an Intel only OS.
     */
    private static int bytesToWord(byte[] bytes, int off) {
        return ((bytes[off + 1] & 0xff) << 8) | (bytes[off] & 0xff);
    }

    private static int bytesToDword(byte[] bytes, int off) {
        return (bytesToWord(bytes, off + 2) << 16) | bytesToWord(bytes, off);
    }

}
问题回答

Sam Brightman s solution is for local files only. I added support for Network files:

  • Windows shortcut (.lnk) parser in Java?
  • http://code.google.com/p/8bits/downloads/detail?name=The_Windows_Shortcut_File_Format.pdf
  • 将此翻译成中文:http://www.javafaq.nu/java-example-code-468.html http://www.javafaq.nu/java-example-code-468.html

    public class LnkParser {
    
    public LnkParser(File f) throws IOException {
        parse(f);
    }
    
    private boolean isDirectory;
    private boolean isLocal;
    
    public boolean isDirectory() {
        return isDirectory;
    }
    
    private String real_file;
    
    public String getRealFilename() {
        return real_file;
    }
    
    private void parse(File f) throws IOException {
        // read the entire file into a byte buffer
        FileInputStream fin = new FileInputStream(f);
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        byte[] buff = new byte[256];
        while (true) {
            int n = fin.read(buff);
            if (n == -1) {
                break;
            }
            bout.write(buff, 0, n);
        }
        fin.close();
        byte[] link = bout.toByteArray();
    
        parseLink(link);
    }
    
    private void parseLink(byte[] link) {
        // get the flags byte
        byte flags = link[0x14];
    
        // get the file attributes byte
        final int file_atts_offset = 0x18;
        byte file_atts = link[file_atts_offset];
        byte is_dir_mask = (byte)0x10;
        if ((file_atts & is_dir_mask) > 0) {
            isDirectory = true;
        } else {
            isDirectory = false;
        }
    
        // if the shell settings are present, skip them
        final int shell_offset = 0x4c;
        final byte has_shell_mask = (byte)0x01;
        int shell_len = 0;
        if ((flags & has_shell_mask) > 0) {
            // the plus 2 accounts for the length marker itself
            shell_len = bytes2short(link, shell_offset) + 2;
        }
    
        // get to the file settings
        int file_start = 0x4c + shell_len;
    
        final int file_location_info_flag_offset_offset = 0x08;
        int file_location_info_flag = link[file_start + file_location_info_flag_offset_offset];
        isLocal = (file_location_info_flag & 2) == 0;
        // get the local volume and local system values
        //final int localVolumeTable_offset_offset = 0x0C;
        final int basename_offset_offset = 0x10;
        final int networkVolumeTable_offset_offset = 0x14;
        final int finalname_offset_offset = 0x18;
        int finalname_offset = link[file_start + finalname_offset_offset] + file_start;
        String finalname = getNullDelimitedString(link, finalname_offset);
        if (isLocal) {
            int basename_offset = link[file_start + basename_offset_offset] + file_start;
            String basename = getNullDelimitedString(link, basename_offset);
            real_file = basename + finalname;
        } else {
            int networkVolumeTable_offset = link[file_start + networkVolumeTable_offset_offset] + file_start;
            int shareName_offset_offset = 0x08;
            int shareName_offset = link[networkVolumeTable_offset + shareName_offset_offset]
                    + networkVolumeTable_offset;
            String shareName = getNullDelimitedString(link, shareName_offset);
            real_file = shareName + "\" + finalname;
        }
    }
    
    private static String getNullDelimitedString(byte[] bytes, int off) {
        int len = 0;
        // count bytes until the null character (0)
        while (true) {
            if (bytes[off + len] == 0) {
                break;
            }
            len++;
        }
        return new String(bytes, off, len);
    }
    
    /*
     * convert two bytes into a short note, this is little endian because it s
     * for an Intel only OS.
     */
    private static int bytes2short(byte[] bytes, int off) {
        return ((bytes[off + 1] & 0xff) << 8) | (bytes[off] & 0xff);
    }
    
    /**
     * Returns the value of the instance variable  isLocal .
     *
     * @return Returns the isLocal.
     */
    public boolean isLocal() {
        return isLocal;
    }
    }
    

I can recommend this repository on GitHub:

https://github.com/BlackOverlord666/mslinks

There I ve found a simple solution to create shortcuts:

ShellLink.createLink("path/to/existing/file.txt", "path/to/the/future/shortcut.lnk");

If you want to read shortcuts:

File shortcut = ...;
String pathToExistingFile = new ShellLink(shortcut).resolveTarget();

如果您想更改快捷方式的图标,请使用:

ShellLink sl = ...;
sl.setIconLocation("/path/to/icon/file");

您可以编辑概述链接的大部分属性,例如工作目录、工具提示文本、图标、命令行参数、热键、创建到局域网共享文件和目录的链接等等…

Hope this helps you :)

Kind regards Josua Frank

计划9汇编程序链接似乎只需要轻微修改即可工作。我认为只需要更改在bytes2short函数中byte强制转换成int时用于防止符号扩展的"& 0xff"。我已添加了http://www.i2s-lab.com/Papers/The_Windows_Shortcut_File_Format.pdf中描述的功能,即连接“路径名的最后一部分”,尽管在实践中我似乎没有使用它。我没有为标题添加任何错误检查或处理网络共享。这是我现在使用的内容:

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.text.DecimalFormat;
import java.text.NumberFormat;

public class LnkParser {

    public LnkParser(File f) throws Exception {
        parse(f);
    }

    private boolean is_dir;

    public boolean isDirectory() {
        return is_dir;
    }

    private String real_file;

    public String getRealFilename() {
        return real_file;
    }

    private void parse(File f) throws Exception {
        // read the entire file into a byte buffer
        FileInputStream fin = new FileInputStream(f);
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        byte[] buff = new byte[256];
        while (true) {
            int n = fin.read(buff);
            if (n == -1) {
                break;
            }
            bout.write(buff, 0, n);
        }
        fin.close();
        byte[] link = bout.toByteArray();

        // get the flags byte
        byte flags = link[0x14];

        // get the file attributes byte
        final int file_atts_offset = 0x18;
        byte file_atts = link[file_atts_offset];
        byte is_dir_mask = (byte) 0x10;
        if ((file_atts & is_dir_mask) > 0) {
            is_dir = true;
        } else {
            is_dir = false;
        }

        // if the shell settings are present, skip them
        final int shell_offset = 0x4c;
        final byte has_shell_mask = (byte) 0x01;
        int shell_len = 0;
        if ((flags & has_shell_mask) > 0) {
            // the plus 2 accounts for the length marker itself
            shell_len = bytes2short(link, shell_offset) + 2;
        }

        // get to the file settings
        int file_start = 0x4c + shell_len;

        // get the local volume and local system values
        final int basename_offset_offset = 0x10;
        final int finalname_offset_offset = 0x18;
        int basename_offset = link[file_start + basename_offset_offset]
                                + file_start;
        int finalname_offset = link[file_start + finalname_offset_offset]
                                + file_start;
        String basename = getNullDelimitedString(link, basename_offset);
        String finalname = getNullDelimitedString(link, finalname_offset);
        real_file = basename + finalname;
    }

    private static String getNullDelimitedString(byte[] bytes, int off) {
        int len = 0;
        // count bytes until the null character (0)
        while (true) {
            if (bytes[off + len] == 0) {
                break;
            }
            len++;
        }
        return new String(bytes, off, len);
    }

    /*
     * convert two bytes into a short note, this is little endian because it s
     * for an Intel only OS.
     */
    private static int bytes2short(byte[] bytes, int off) {
        return ((bytes[off + 1] & 0xff) << 8) | (bytes[off] & 0xff);
    }
}

我也曾经在Java上开发过.lnk文件(现在没时间了)。我的代码在这里: 这里

它有点凌乱(一些测试垃圾),但是本地和网络解析工作得很好。创建链接也已实现。请测试并发送补丁给我。

解析示例:

Shortcut scut = Shortcut.loadShortcut(new File("C:\t.lnk"));
System.out.println(scut.toString());

创建新链接:

Shortcut scut = new Shortcut(new File("C:\temp"));
OutputStream os = new FileOutputStream("C:\t.lnk");
os.write(scut.getBytes());
os.flush();
os.close();

The solution of @Code Bling does not work for me for Files in the User directory.
For Example "C:/Users/Username/Filename.txt".
The reason for that is: in The_Windows_Shortcut_File_Format.pdf

that was mentioned by @Stefan Cordes on page 6 it says that only the first 2 bits are important for volumes info. All other bits might be filled with random garbage when the first bit of volumes info is "0".

如果这样说:

isLocal = (file_location_info_flag & 2) == 0;

then file_location_info_flag might be "3". This file is still local but this line of code assigns false to isLocal.

所以我建议对 @Code Bling 的代码进行以下调整:

isLocal = (file_location_info_flag & 1) == 1;

这个短代码非常有用...

但需要进行两个修复:

  • 如果名称不以“.lnk”结尾,则isPotentialValidLink改进不加载文件。 (Translated using Google Translate)

      public static boolean isPotentialValidLink(final File file) {
        final int minimum_length = 0x64;
        boolean isPotentiallyValid = false;
        if (file.getName().toLowerCase().endsWith(".lnk"))
            try (final InputStream fis = new FileInputStream(file)) {
                isPotentiallyValid = file.isFile() && fis.available() >= minimum_length && isMagicPresent(getBytes(fis, 32));
            } catch (Exception e) {
                // forget it
            }
        return isPotentiallyValid;
      }
    
  • 抵消量必须使用32位计算而不仅仅是一个字节。

     final int finalname_offset = bytesToDword(link,file_start + finalname_offset_offset) + file_start;
     final int basename_offset = bytesToDword(link,file_start + basename_offset_offset) + file_start;
    

我找到了其他非专业的技巧来完成我的工作。

File file=new File("C:\ProgramData\Microsoft\Windows\Start Menu\Programs\TeamViewer.lnk");//shortcut link
            FileInputStream stream=new FileInputStream(file);
            DataInputStream st=new DataInputStream(stream);
           byte[] bytes=new byte[(int)stream.getChannel().size()];
           stream.read(bytes);
           String data=new String(bytes);
           int i1=data.indexOf("C:\Program Files");
           int i2=data.indexOf(".exe",i1);
           System.out.println(data.substring(i1, i2+4));

给定的代码工作得很好,但有一个错误。Java字节是从-128到127的有符号值。我们需要获取从0到255的无符号值以获得正确的结果。只需将bytes2short函数更改如下:

static int bytes2short(byte[] bytes, int off) {
    int low = (bytes[off]<0 ? bytes[off]+256 : bytes[off]);
    int high = (bytes[off+1]<0 ? bytes[off+1]+256 : bytes[off+1])<<8;
    return 0 | low | high;
}




相关问题
热门标签