mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2026-05-23 00:00:05 -04:00
478 lines
16 KiB
Java
Executable File
478 lines
16 KiB
Java
Executable File
/*
|
|
* Copyright (c) 2008, Harald Kuhr
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions are met:
|
|
*
|
|
* * Redistributions of source code must retain the above copyright notice, this
|
|
* list of conditions and the following disclaimer.
|
|
*
|
|
* * Redistributions in binary form must reproduce the above copyright notice,
|
|
* this list of conditions and the following disclaimer in the documentation
|
|
* and/or other materials provided with the distribution.
|
|
*
|
|
* * Neither the name of the copyright holder nor the names of its
|
|
* contributors may be used to endorse or promote products derived from
|
|
* this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
package com.twelvemonkeys.io;
|
|
|
|
import java.io.*;
|
|
import java.util.Arrays;
|
|
|
|
/**
|
|
* A {@code File} implementation that resolves the Windows {@code .lnk} files as symbolic links.
|
|
* <p>
|
|
* This class is based on example code from
|
|
* <a href="http://www.oreilly.com/catalog/swinghks/index.html">Swing Hacks</a>,
|
|
* By Joshua Marinacci, Chris Adamson (O'Reilly, ISBN: 0-596-00907-0), Hack 30.
|
|
* </p>
|
|
*
|
|
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
|
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/Win32Lnk.java#2 $
|
|
*/
|
|
final class Win32Lnk extends File {
|
|
private final static byte[] LNK_MAGIC = {
|
|
'L', 0x00, 0x00, 0x00, // Magic
|
|
};
|
|
private final static byte[] LNK_GUID = {
|
|
0x01, 0x14, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, // Shell Link GUID
|
|
(byte) 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 'F'
|
|
};
|
|
|
|
private final File target;
|
|
|
|
private static final int FLAG_ITEM_ID_LIST = 0x01;
|
|
private static final int FLAG_FILE_LOC_INFO = 0x02;
|
|
private static final int FLAG_DESC_STRING = 0x04;
|
|
private static final int FLAG_REL_PATH_STRING = 0x08;
|
|
private static final int FLAG_WORKING_DIRECTORY = 0x10;
|
|
private static final int FLAG_COMMAND_LINE_ARGS = 0x20;
|
|
private static final int FLAG_ICON_FILENAME = 0x40;
|
|
private static final int FLAG_ADDITIONAL_INFO = 0x80;
|
|
|
|
private Win32Lnk(final String pPath) throws IOException {
|
|
super(pPath);
|
|
File target = parse(this);
|
|
if (target == this) {
|
|
// NOTE: This is a workaround
|
|
// target = this causes infinite loops in some methods
|
|
target = new File(pPath);
|
|
}
|
|
this.target = target;
|
|
}
|
|
|
|
Win32Lnk(final File pPath) throws IOException {
|
|
this(pPath.getPath());
|
|
}
|
|
|
|
/**
|
|
* Parses a {@code .lnk} file to find the real file.
|
|
*
|
|
* @param pPath the path to the {@code .lnk} file
|
|
* @return a new file object that
|
|
* @throws java.io.IOException if the {@code .lnk} cannot be parsed
|
|
*/
|
|
static File parse(final File pPath) throws IOException {
|
|
if (!pPath.getName().endsWith(".lnk")) {
|
|
return pPath;
|
|
}
|
|
|
|
File result = pPath;
|
|
|
|
LittleEndianDataInputStream in = new LittleEndianDataInputStream(new BufferedInputStream(new FileInputStream(pPath)));
|
|
try {
|
|
byte[] magic = new byte[4];
|
|
in.readFully(magic);
|
|
|
|
byte[] guid = new byte[16];
|
|
in.readFully(guid);
|
|
|
|
if (!(Arrays.equals(LNK_MAGIC, magic) && Arrays.equals(LNK_GUID, guid))) {
|
|
//System.out.println("Not a symlink");
|
|
// Not a symlink
|
|
return pPath;
|
|
}
|
|
|
|
// Get the flags
|
|
int flags = in.readInt();
|
|
//System.out.println("flags: " + Integer.toBinaryString(flags & 0xff));
|
|
|
|
// Get to the file settings
|
|
/*int attributes = */in.readInt();
|
|
|
|
// File attributes
|
|
// 0 Target is read only.
|
|
// 1 Target is hidden.
|
|
// 2 Target is a system file.
|
|
// 3 Target is a volume label. (Not possible)
|
|
// 4 Target is a directory.
|
|
// 5 Target has been modified since last backup. (archive)
|
|
// 6 Target is encrypted (NTFS EFS)
|
|
// 7 Target is Normal??
|
|
// 8 Target is temporary.
|
|
// 9 Target is a sparse file.
|
|
// 10 Target has reparse point data.
|
|
// 11 Target is compressed.
|
|
// 12 Target is offline.
|
|
//System.out.println("attributes: " + Integer.toBinaryString(attributes));
|
|
// NOTE: Cygwin .lnks are not directory links, can't rely on this.. :-/
|
|
|
|
in.skipBytes(48); // TODO: Make sense of this data...
|
|
|
|
// Skipped data:
|
|
// long time 1 (creation)
|
|
// long time 2 (modification)
|
|
// long time 3 (last access)
|
|
// int file length
|
|
// int icon number
|
|
// int ShowVnd value
|
|
// int hotkey
|
|
// int, int - unknown: 0,0
|
|
|
|
// If the shell settings are present, skip them
|
|
if ((flags & FLAG_ITEM_ID_LIST) != 0) {
|
|
// Shell Item Id List present
|
|
//System.out.println("Shell Item Id List present");
|
|
int shellLen = in.readShort(); // Short
|
|
//System.out.println("shellLen: " + shellLen);
|
|
|
|
// TODO: Probably need to parse this data, to determine
|
|
// Cygwin folders...
|
|
|
|
/*
|
|
int read = 2;
|
|
int itemLen = in.readShort();
|
|
while (itemLen > 0) {
|
|
System.out.println("--> ITEM: " + itemLen);
|
|
|
|
BufferedReader reader = new BufferedReader(new InputStreamReader(new SubStream(in, itemLen - 2)));
|
|
//byte[] itemBytes = new byte[itemLen - 2]; // NOTE: Lenght included
|
|
//in.readFully(itemBytes);
|
|
|
|
String item = reader.readLine();
|
|
System.out.println("item: \"" + item + "\"");
|
|
|
|
itemLen = in.readShort();
|
|
read += itemLen;
|
|
}
|
|
|
|
System.out.println("read: " + read);
|
|
*/
|
|
|
|
in.skipBytes(shellLen);
|
|
}
|
|
|
|
if ((flags & FLAG_FILE_LOC_INFO) != 0) {
|
|
// File Location Info Table present
|
|
//System.out.println("File Location Info Table present");
|
|
|
|
// 0h 1 dword This is the total length of this structure and all following data
|
|
// 4h 1 dword This is a pointer to first offset after this structure. 1Ch
|
|
// 8h 1 dword Flags
|
|
// Ch 1 dword Offset of local volume info
|
|
// 10h 1 dword Offset of base pathname on local system
|
|
// 14h 1 dword Offset of network volume info
|
|
// 18h 1 dword Offset of remaining pathname
|
|
|
|
// Flags:
|
|
// Bit Meaning
|
|
// 0 Available on a local volume
|
|
// 1 Available on a network share
|
|
// TODO: Make sure the path is on a local disk, etc..
|
|
|
|
int tableLen = in.readInt(); // Int
|
|
//System.out.println("tableLen: " + tableLen);
|
|
|
|
in.readInt(); // Skip
|
|
|
|
int locFlags = in.readInt();
|
|
//System.out.println("locFlags: " + Integer.toBinaryString(locFlags));
|
|
if ((locFlags & 0x01) != 0) {
|
|
//System.out.println("Available local");
|
|
}
|
|
if ((locFlags & 0x02) != 0) {
|
|
//System.err.println("Available on network path");
|
|
}
|
|
|
|
// Get the local volume and local system values
|
|
in.skipBytes(4); // TODO: see above for structure
|
|
|
|
int localSysOff = in.readInt();
|
|
//System.out.println("localSysOff: " + localSysOff);
|
|
in.skipBytes(localSysOff - 20); // Relative to start of chunk
|
|
|
|
byte[] pathBytes = new byte[tableLen - localSysOff - 1];
|
|
in.readFully(pathBytes, 0, pathBytes.length);
|
|
String path = new String(pathBytes, 0, pathBytes.length - 1);
|
|
/*
|
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
|
byte read;
|
|
// Read bytes until the null (0) character
|
|
while (true) {
|
|
read = in.readByte();
|
|
if (read == 0) {
|
|
break;
|
|
}
|
|
bytes.write(read & 0xff);
|
|
}
|
|
|
|
String path = new String(bytes.toByteArray(), 0, bytes.size());
|
|
//*/
|
|
|
|
// Recurse to end of link chain
|
|
// TODO: This may cause endless loop if cyclic chain...
|
|
//System.out.println("path: \"" + path + "\"");
|
|
try {
|
|
result = parse(new File(path));
|
|
}
|
|
catch (StackOverflowError e) {
|
|
throw new IOException("Cannot resolve cyclic link: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
if ((flags & FLAG_DESC_STRING) != 0) {
|
|
// Description String present, skip it.
|
|
//System.out.println("Description String present");
|
|
|
|
// The string length is the first word which must also be skipped.
|
|
int descLen = in.readShort();
|
|
//System.out.println("descLen: " + descLen);
|
|
|
|
byte[] descBytes = new byte[descLen];
|
|
in.readFully(descBytes, 0, descLen);
|
|
|
|
//String desc = new String(descBytes, 0, descLen);
|
|
//System.out.println("desc: " + desc);
|
|
}
|
|
|
|
if ((flags & FLAG_REL_PATH_STRING) != 0) {
|
|
// Relative Path String present
|
|
//System.out.println("Relative Path String present");
|
|
|
|
// The string length is the first word which must also be skipped.
|
|
int pathLen = in.readShort();
|
|
//System.out.println("pathLen: " + pathLen);
|
|
|
|
byte[] pathBytes = new byte[pathLen];
|
|
in.readFully(pathBytes, 0, pathLen);
|
|
|
|
String path = new String(pathBytes, 0, pathLen);
|
|
|
|
// TODO: This may cause endless loop if cyclic chain...
|
|
//System.out.println("path: \"" + path + "\"");
|
|
if (result == pPath) {
|
|
try {
|
|
result = parse(new File(pPath.getParentFile(), path));
|
|
}
|
|
catch (StackOverflowError e) {
|
|
throw new IOException("Cannot resolve cyclic link: " + e.getMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
if ((flags & FLAG_WORKING_DIRECTORY) != 0) {
|
|
//System.out.println("Working Directory present");
|
|
}
|
|
if ((flags & FLAG_COMMAND_LINE_ARGS) != 0) {
|
|
//System.out.println("Command Line Arguments present");
|
|
// NOTE: This means this .lnk is not a folder, don't follow
|
|
result = pPath;
|
|
}
|
|
if ((flags & FLAG_ICON_FILENAME) != 0) {
|
|
//System.out.println("Icon Filename present");
|
|
}
|
|
if ((flags & FLAG_ADDITIONAL_INFO) != 0) {
|
|
//System.out.println("Additional Info present");
|
|
}
|
|
}
|
|
finally {
|
|
in.close();
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/*
|
|
private static String getNullDelimitedString(byte[] bytes, int off) {
|
|
int len = 0;
|
|
// Count bytes until the null (0) character
|
|
while (true) {
|
|
if (bytes[off + len] == 0) {
|
|
break;
|
|
}
|
|
len++;
|
|
}
|
|
|
|
System.err.println("--> " + len);
|
|
|
|
return new String(bytes, off, len);
|
|
}
|
|
*/
|
|
|
|
/**
|
|
* Converts two bytes into a short.
|
|
* <p>
|
|
* NOTE: this is little endian because it's for an
|
|
* Intel only OS
|
|
* </p>
|
|
*
|
|
* @ param bytes
|
|
* @ param off
|
|
* @return the bytes as a short.
|
|
*/
|
|
/*
|
|
private static int bytes2short(byte[] bytes, int off) {
|
|
return ((bytes[off + 1] & 0xff) << 8) | (bytes[off] & 0xff);
|
|
}
|
|
*/
|
|
|
|
public File getTarget() {
|
|
return target;
|
|
}
|
|
|
|
// java.io.File overrides below
|
|
|
|
@Override
|
|
public boolean isDirectory() {
|
|
return target.isDirectory();
|
|
}
|
|
|
|
@Override
|
|
public boolean canRead() {
|
|
return target.canRead();
|
|
}
|
|
|
|
@Override
|
|
public boolean canWrite() {
|
|
return target.canWrite();
|
|
}
|
|
|
|
// NOTE: equals is implemented using compareto == 0
|
|
/*
|
|
public int compareTo(File pathname) {
|
|
// TODO: Verify this
|
|
// Probably not a good idea, as it IS NOT THE SAME file
|
|
// It's probably better to not override
|
|
return target.compareTo(pathname);
|
|
}
|
|
*/
|
|
|
|
// Should probably never allow creating a new .lnk
|
|
// public boolean createNewFile() throws IOException
|
|
|
|
// Deletes only the .lnk
|
|
// public boolean delete() {
|
|
//public void deleteOnExit() {
|
|
|
|
@Override
|
|
public boolean exists() {
|
|
return target.exists();
|
|
}
|
|
|
|
// A .lnk may be absolute
|
|
//public File getAbsoluteFile() {
|
|
//public String getAbsolutePath() {
|
|
|
|
// Theses should be resolved according to the API (for Unix).
|
|
@Override
|
|
public File getCanonicalFile() throws IOException {
|
|
return target.getCanonicalFile();
|
|
}
|
|
|
|
@Override
|
|
public String getCanonicalPath() throws IOException {
|
|
return target.getCanonicalPath();
|
|
}
|
|
|
|
//public String getName() {
|
|
|
|
// I guess the parent should be the parent of the .lnk, not the target
|
|
//public String getParent() {
|
|
//public File getParentFile() {
|
|
|
|
// public boolean isAbsolute() {
|
|
@Override
|
|
public boolean isFile() {
|
|
return target.isFile();
|
|
}
|
|
|
|
@Override
|
|
public boolean isHidden() {
|
|
return target.isHidden();
|
|
}
|
|
|
|
@Override
|
|
public long lastModified() {
|
|
return target.lastModified();
|
|
}
|
|
|
|
@Override
|
|
public long length() {
|
|
return target.length();
|
|
}
|
|
|
|
@Override
|
|
public String[] list() {
|
|
return target.list();
|
|
}
|
|
|
|
@Override
|
|
public String[] list(final FilenameFilter filter) {
|
|
return target.list(filter);
|
|
}
|
|
|
|
@Override
|
|
public File[] listFiles() {
|
|
return Win32File.wrap(target.listFiles());
|
|
}
|
|
|
|
@Override
|
|
public File[] listFiles(final FileFilter filter) {
|
|
return Win32File.wrap(target.listFiles(filter));
|
|
}
|
|
|
|
@Override
|
|
public File[] listFiles(final FilenameFilter filter) {
|
|
return Win32File.wrap(target.listFiles(filter));
|
|
}
|
|
|
|
// Makes no sense, does it?
|
|
//public boolean mkdir() {
|
|
//public boolean mkdirs() {
|
|
|
|
// Only rename the lnk
|
|
//public boolean renameTo(File dest) {
|
|
|
|
@Override
|
|
public boolean setLastModified(long time) {
|
|
return target.setLastModified(time);
|
|
}
|
|
|
|
@Override
|
|
public boolean setReadOnly() {
|
|
return target.setReadOnly();
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
if (target.equals(this)) {
|
|
return super.toString();
|
|
}
|
|
return super.toString() + " -> " + target.toString();
|
|
}
|
|
}
|