Zarkonnen
Zarkonnen

Reputation: 22478

Windows shortcut (.lnk) parser in Java?

I'm currently using Win32ShellFolderManager2 and ShellFolder.getLinkLocation to resolve windows shortcuts in Java. Unfortunately, if the Java program is running as a service under Vista, getLinkLocation, this does not work. Specifically, I get an exception stating "Could not get shell folder ID list".

Searching the web does turn up mentions of this error message, but always in connection with JFileChooser. I'm not using JFileChooser, I just need to resolve a .lnk file to its destination.

Does anyone know of a 3rd-party parser for .lnk files written in Java I could use?

I've since found unofficial documentation for the .lnk format here, but I'd rather not have to do the work if anyone has done it before, since the format is rather scary.

Upvotes: 27

Views: 28384

Answers (10)

DuncG
DuncG

Reputation: 15086

With JDK22+ you can use pure Java implementation that uses the Foreign Function and Memory API to call directly to the built in Windows libraries for shell link. There should be no errors with binary incompatibility (unless Microsoft adds one), which would be a risk for all other approaches.

This example program looks up the Windows CLSID for ShellLink and retrieves the IShellLinkW and IPersistFile COM interfaces to load a Windows shortcut ".lnk" file. Then you just need to retrieve all properties eg GetPath. This sample is quite long so for sake of brevity I've left out reading other properties such as arguments, and other operations such as Save.

public record WindowsShortcut(Path lnk, Path target) { }

/** Load a shortcut .lnk file using COM CLSID_ShellLink object */
public static WindowsShortcut load(Path file) throws IOException {

    // Performs the various COM operations using same native memory arena
    try(Arena arena = Arena.ofConfined()) {

        // Global GUIDs definitions are part of uuid.lib not bound in DLL
        var CLSID_ShellLink  = arena.allocateFrom(JAVA_BYTE, // "{00021401-0000-0000-C000-000000000046}"
                new byte[] {0x1,0x14,0x2,0x0,  0x0,0x0,  0x0,0x0,  (byte) 0xC0,0x0,0x0,0x0,0x0,0x0,0x0,0x46});

        var IID_IShellLink   = arena.allocateFrom(JAVA_BYTE, // "{000214F9-0000-0000-C000-000000000046}"
                new byte[] {(byte)0xF9,0x14,0x2,0x0,0x0,0x0,0x0,0x0,(byte)0xC0,0x0,0x0,0x0,0x0,0x0,0x0,0x46});

        // Interface IID_Persist
        var IID_IPersistFile = arena.allocateFrom(JAVA_BYTE, // "{0000010B-0000-0000-C000-000000000046}"
                new byte[] {0xB,0x1,0x0,0x0,0x0,0x0,0x0,0x0,(byte)0xC0,0x0,0x0,0x0,0x0,0x0,0x0,0x46});

        // Allocate memory to receive pointer to ADDRESS, re-used
        MemorySegment pAddress = arena.allocateFrom(Shortcut_h.C_POINTER, MemorySegment.NULL);

        // String for the pathname of lnk, resolved to absolute path
        MemorySegment pszFileName = arena.allocateFrom(file.toRealPath().toString(), StandardCharsets.UTF_16LE);

        // Setup COM:
        int hRes = Shortcut_h.CoInitialize(MemorySegment.NULL);
        Objects.checkIndex(hRes, 2); // checks Shortcut_h.S_OK(), Shortcut_h.S_FALSE()
        try {
            // Get a pointer to the IShellLink interface.
            int cciRes = Shortcut_h.CoCreateInstance(CLSID_ShellLink, /*pUnkOuter*/MemorySegment.NULL, /*dwClsContext*/Shortcut_h.CLSCTX_INPROC_SERVER(), /*riid*/IID_IShellLink, pAddress);
            Objects.checkIndex(cciRes, 1);

            // Wrap as Java ShellLink
            try (var iShellLink = new JShellLink(pAddress.get(ADDRESS, 0))) {

                // Query IShellLink for the IPersistFile interface for load/save
                try (var iPersist = new JPersistFile(/*IPersistFile*/ iShellLink.QueryInterface(IID_IPersistFile, pAddress))) {

                    // Load the shortcut lnk file
                    iPersist.Load(pszFileName);

                    // Load properties of LNK: TODO: Setup other fields here such as Arguments IDPath etc
                    String target = iShellLink.GetPath(arena, arena.allocate(JAVA_BYTE, Shortcut_h.MAX_PATH()));

                    return new WindowsShortcut(file, target != null ? Path.of(target) : null);
                }
            }
        } finally {
            Shortcut_h.CoUninitialize();
        }
    }
}

public static void main(String ... args) throws IOException {
    var shortcut = WindowsShortcut.load(Path.of(args[0]));
    System.out.println("shortcut => "+shortcut);
}

In order to get the simple method above, I refactored various actions into Java helper classes so can re-use this style of callbacks elsewhere in my own code:

/** COM definition holds pointers to OBJ and VTABLE (matching the size for IXYZ table) */
record COM(MemorySegment obj, MemorySegment vtab) {}

/** JUnknown handles all operations on IUnknown */
class JUnknown implements AutoCloseable {
    protected final COM com;

    /** Derived classes must use this constructor with COM object definition with appropriate memory vtable size. */
    protected JUnknown(final MemorySegment comObj, long vTableSize) {
        // retrieve IUnknown structure from the provided comObj segment
        // retrieve vtable of size appropriate to the caller eg I{xyz}Vtbl.sizeof()
        this.com = new COM(comObj, IUnknown.lpVtbl(comObj.reinterpret(IUnknown.sizeof())).reinterpret(vTableSize));
    }
    /** IUnknown Release */
    public int Release() {
        return IUnknownVtbl.Release.invoke(IUnknownVtbl.Release(com.vtab()), com.obj());
    }
    /** IUnknown QueryInterface -> returns requested pInterface */
    public MemorySegment QueryInterface(MemorySegment riid, MemorySegment ppv) {
        int qiRes = IUnknownVtbl.QueryInterface.invoke(IUnknownVtbl.QueryInterface(com.vtab()), com.obj(), riid, ppv);
        Objects.checkIndex(qiRes, 1); // check 0
        return ppv.get(Shortcut_h.C_POINTER, 0);
    }
    /** Autoclose - calls Release to avoid leaking refs to COM objects. */
    public void close() {
        Release();
    }
}

/** JShellLink handles all operations on IShellLink */
class JShellLink extends JUnknown {
    /** Setup IShellLink for the COM object given */
    public JShellLink(final MemorySegment comObj) {
        super(comObj, IShellLinkWVtbl.sizeof());
    }
    /**IShellLinkW::GetPath method (shobjidl_core.h) */
    public String GetPath(Arena arena, MemorySegment path83) {
        // Get the path to the link target which is converted from 8.3 format
        // An error here might be target file not found
        // Ignores WIN32_FIND_DATAA *pfd for now
        int hRes = IShellLinkWVtbl.GetPath.invoke(IShellLinkWVtbl.GetPath(com.vtab()), com.obj(), path83, (int)path83.byteSize(), /*pfd*/ MemorySegment.NULL, /*fFlags*/Shortcut_h.SLGP_SHORTPATH());
        Objects.checkIndex(hRes, 2); // check 0/1

        String target = path83.getString(0, StandardCharsets.UTF_16LE);
        if (hRes == Shortcut_h.S_OK()) {
            // LNK  Target appears to be 8.3 so convert it to long format
            int nChars = Shortcut_h.MAX_PATH();
            final MemorySegment lpszLongPath = arena.allocate(Shortcut_h.WCHAR, nChars); // Max: Kernel32_h.UNICODE_STRING_MAX_CHARS()

            int len = Shortcut_h.GetLongPathNameW(path83, lpszLongPath, nChars);
            // len returned is = number of chars copied OR size of buffer required to hold "number of chars + NULL
            Objects.checkIndex(len, nChars); // check in bounds
            target = lpszLongPath.getString(0, StandardCharsets.UTF_16LE);
        }
        return target;
    }
}

/** JPersistFile handles all operations on IPersistFile */
class JPersistFile extends JUnknown {
    /** Setup IPersistFile for the COM object given */
    public JPersistFile(final MemorySegment comObj) {
        super(comObj, IPersistFileVtbl.sizeof());
    }
    /** IPersistFile::Load method (objidl.h) */
    public void Load(MemorySegment pszFileName) {
        int hRes = IPersistFileVtbl.Load.invoke(IPersistFileVtbl.Load(com.vtab()), com.obj(), pszFileName, Shortcut_h.STGM_READ());
        Objects.checkIndex(hRes, 1); // check 0
    }
}

Finally, all of the above requires more code which is generated by jextract to access the binary layouts of each Windows API data structure used. You of course need Windows API header files, which is available if you download Visual Studio. The command I used was:

jextract -lshell32 -lkernel32 -lole32 -t gen.code.shortcut --output junit --dump-includes Shortcut.sym

Then edited Shortcut.sym to be the symbols used above keeping only required symbol of each Shortcut_h.xxxx call referenced, then re-run with:

jextract -lshell32 -lkernel32 -lole32 -t gen.code.shortcut --output junit --header-class-name Shortcut_h @Shortcut.sym "<shlobj_core.h>"

Upvotes: 1

Codebling
Codebling

Reputation: 11382

Use mslinks library

mslinks is an open source, well-maintained, well-tested project whose author has put considerable effort into reverse-engineering the shortcut format. Consider using it instead of any of the code listed in the answers.

If that library does not suit your needs, read on...

Code from various other answers

I've combined some of the code from the various answers, and added comments (some explanation as well as credit to each contributor so far), additional check on the file magic, a quick test to see if a given file might be a valid link (without reading all of the bytes), a fix to throw a ParseException with appropriate message instead of ArrayIndexOutOfBoundsException if the file is too small and did some general clean-up.

Source here (if you have any changes, push them right to the 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 &lt;[email protected]&gt;
 *   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);
    }

}

Upvotes: 31

Abrar Malekji
Abrar Malekji

Reputation: 111

I found other non-professional technique. getting my job done.

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

Upvotes: 1

JS Lair
JS Lair

Reputation: 11

This short code is really usefull...

But two fixes are needed:

  • the isPotentialValidLink improved not to load file if name doesn't end with ".lnk"

      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;
      }
    
  • the offset has to be computed with 32bits not only a byte...

     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;
    

Upvotes: 1

Stefan Cordes
Stefan Cordes

Reputation:

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

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

Upvotes: 15

Josua Frank
Josua Frank

Reputation: 339

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

If you want to change the icon of the shortcut, use:

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

You can edit most properties of the shortcutlink such as working directory, tooltip text, icon, command line arguments, hotkeys, create links to LAN shared files and directories and much more...

Hope this helps you :)

Kind regards Josua Frank

Upvotes: 8

Naxos84
Naxos84

Reputation: 2028

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".

So if it comes to:

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.

So i suggest the following adjustment to @Code Bling's code:

isLocal = (file_location_info_flag & 1) == 1;

Upvotes: 1

kac_ani
kac_ani

Reputation:

I've also worked( now have no time for that) on '.lnk' in Java. My code is here

It's little messy( some testing trash) but local and network parsing works good. Creating links is implemented too. Please test and send me patches.

Parsing example:

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

Creating new link:

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

Upvotes: 3

ADDPCs
ADDPCs

Reputation:

The given code works well, but has a bug. A java byte is a signed value from -128 to 127. We want an unsigned value from 0 to 255 to get the correct results. Just change the bytes2short function as follows:

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

Upvotes: 0

Sam Brightman
Sam Brightman

Reputation: 2950

The code plan9assembler linked to appears to work with minor modification. I think it's just the "& 0xff" to prevent sign extension when bytes are upcast to ints in the bytes2short function that need changing. I've added the functionality described in http://www.i2s-lab.com/Papers/The_Windows_Shortcut_File_Format.pdf to concatenate the "final part of the pathname" even though in practice this doesn't seem to be used in my examples. I've not added any error checking to the header or dealt with network shares. Here's what I'm using now:

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

Upvotes: 3

Related Questions