Java中的Windows快捷方式(.lnk)解析器?

问题描述 投票:21回答:8

我目前正在使用Win32ShellFolderManager2ShellFolder.getLinkLocation来解析Java中的Windows快捷方式。不幸的是,如果Java程序在Vista,getLinkLocation下作为服务运行,这不起作用。具体来说,我得到一个异常,说明“无法获取shell文件夹ID列表”。

搜索网络确实会出现此错误消息,但始终与JFileChooser有关。我没有使用JFileChooser,我只需要将.lnk文件解析到目的地。

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

我已经找到.lnk格式here的非官方文档,但如果有人之前已经做过,我宁愿不必做这项工作,因为格式相当可怕。

java windows windows-vista shortcuts lnk
8个回答
24
投票

添加了评论(一些解释以及到目前为止每个贡献者的信用),额外检查文件魔术,快速测试以查看给定文件是否可能是有效链接(不读取所有字节),修复抛出如果文件太小,则使用适当的消息而不是ArrayIndexOutOfBoundsException的ParseException进行一些常规清理。

来源here(如果你有任何变化,请将它们推向GitHub repo / project

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 <[email protected]>
 *   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);
    }

}

12
投票

Sam Brightman's solution仅适用于本地文件。我添加了对网络文件的支持:

  • 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 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; } }

4
投票

我可以在GitHub上推荐这个存储库:

https://github.com/BlackOverlord666/mslinks

在那里,我找到了一个简单的解决方案来创建快捷方式:

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

如果您想阅读快捷方式:

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

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

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

您可以编辑快捷链接的大多数属性,例如工作目录,工具提示文本,图标,命令行参数,热键,创建LAN共享文件和目录的链接等等...

希望这可以帮助你:)

亲切的问候Josua Frank


3
投票

我在Java中的'.lnk'上也工作过(现在没有时间)。我的代码是here

它有点乱(有些测试垃圾),但本地和网络解析效果很好。也实现了创建链接。请测试并发送补丁。

解析示例:

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();

2
投票

链接到的代码plan9assembler似乎只是稍作修改。我认为只有“& 0xff”才能防止符号扩展,当bytes在int函数中向bytes2shorts上升时需要改变。我已经添加了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);
    }
}

1
投票

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

但需要两个修复:

  • 如果name不以“.lnk”结尾,则isPotentialValidLink改进不加载文件 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;

0
投票

给定的代码运行良好,但有一个错误。 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;
}

0
投票

@Code Bling的解决方案不适用于User目录中的Files。 例如“C:/Users/Username/Filename.txt”。 原因是:在The_Windows_Shortcut_File_Format.pdf

@Stefan Cordes在第6页提到它说只有前2位对于卷信息很重要。当卷信息的第一位为“0”时,所有其他位可能被随机垃圾填充。

所以,如果涉及到:

isLocal = (file_location_info_flag & 2) == 0;

然后file_location_info_flag可能是“3”。此文件仍然是本地的,但是这行代码将false分配给isLocal

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

isLocal = (file_location_info_flag & 1) == 1;
© www.soinside.com 2019 - 2024. All rights reserved.