views:

2932

answers:

9

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.

A: 

http://www.javafaq.nu/java-example-code-468.html

HTH

plan9assembler
Unfortunately, that code doesn't work. :(
Zarkonnen
A: 

Unfortunately, http://www.javafaq.nu/java-example-code-468.html doesn't work.

Zarkonnen
+1  A: 

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);
    }
}
Sam Brightman
+1  A: 

Above solution is for local files only. I added support for Network files:

http://stackoverflow.com/questions/309495/windows-shortcut-lnk-parser-in-java http://www.i2s-lab.com/Papers/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;
}

}

works like a charm thanks !
Peter
A: 

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;
}
+2  A: 

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

Hi all,

I've tested all solutions proposed here and no one works with my lnk files, created by cygwin when I untar an archive containing sym links created on a linux box. I got a java.lang.ArrayIndexOutOfBoundsException in getNullDelimitedString() method.

Does anyone knows how to have this lnk parser working with this kind of sym links ?

Thanks a lot,

Eddy

Eddy
This should be a new question.
Thomas Owens
No it shouldn't. It means the code here is wrong.
Joshua
NOTICE: If your .lnk parser is correct, parsing a cygwin .lnk file works as cygwin .lnk files simply have a reserved parameter filled out.
Joshua
A: 

Hello may I ask where class Shortcut i'll be grateful if any one answer me Thanks in advance

A.Ghazy