From e6fbaf24bf58266b9b829889c3e83c390f097f95 Mon Sep 17 00:00:00 2001
From: Dmitry Shamrikov <bivis08@gmail.com>
Date: Thu, 30 Jan 2014 23:43:39 +0600
Subject: [PATCH] LinkInfo reading/writing

---
 src/io/ByteReader.java                    |  35 ++++
 src/io/ByteWriter.java                    |  12 ++
 src/io/Bytes.java                         |   8 +-
 src/mslinks/LinkInfo.java                 | 232 ++++++++++++++++++++++
 src/mslinks/LinkTargetIDList.java         |   8 +-
 src/mslinks/Main.java                     |   8 +-
 src/mslinks/Serializable.java             |   9 +
 src/mslinks/ShellLink.java                |  34 ++--
 src/mslinks/ShellLinkHeader.java          |  10 +-
 src/mslinks/data/BitSet32.java            |   4 +-
 src/mslinks/data/CNRLink.java             | 201 +++++++++++++++++++
 src/mslinks/data/CNRLinkFlags.java        |  33 +++
 src/mslinks/data/FileAttributesFlags.java |   9 +
 src/mslinks/data/Filetime.java            |   4 +-
 src/mslinks/data/GUID.java                |   4 +-
 src/mslinks/data/HotKeyFlags.java         |   4 +-
 src/mslinks/data/LinkFlags.java           |   9 +
 src/mslinks/data/LinkInfoFlags.java       |  32 +++
 src/mslinks/data/VolumeID.java            | 125 ++++++++++++
 test.lnk                                  | Bin 559 -> 923 bytes
 testlink.lnk                              | Bin 1353 -> 1382 bytes
 testlink2.lnk                             | Bin 0 -> 1325 bytes
 testlink3.lnk                             | Bin 0 -> 2321 bytes
 testlink4.lnk                             | Bin 0 -> 2184 bytes
 testlink5.lnk                             | Bin 0 -> 2037 bytes
 testlink6.lnk                             | Bin 0 -> 2098 bytes
 26 files changed, 742 insertions(+), 39 deletions(-)
 create mode 100644 src/mslinks/LinkInfo.java
 create mode 100644 src/mslinks/Serializable.java
 create mode 100644 src/mslinks/data/CNRLink.java
 create mode 100644 src/mslinks/data/CNRLinkFlags.java
 create mode 100644 src/mslinks/data/LinkInfoFlags.java
 create mode 100644 src/mslinks/data/VolumeID.java
 create mode 100644 testlink2.lnk
 create mode 100644 testlink3.lnk
 create mode 100644 testlink4.lnk
 create mode 100644 testlink5.lnk
 create mode 100644 testlink6.lnk

diff --git a/src/io/ByteReader.java b/src/io/ByteReader.java
index 495a8b6..96a30fb 100644
--- a/src/io/ByteReader.java
+++ b/src/io/ByteReader.java
@@ -44,6 +44,13 @@ public class ByteReader extends InputStream {
 		return pos;
 	}
 	
+	public boolean seek(int n) throws IOException {
+		if (n == 0) return false;
+		for (int i=0; i<n; i++)
+			read();		
+		return true;
+	}
+	
 	@Override
 	public int read() throws IOException {
 		pos++;
@@ -133,6 +140,34 @@ public class ByteReader extends InputStream {
 		else 
 			return b7 | (b6 << 8) | (b5 << 16) | (b4 << 24) | (b3 << 32) | (b2 << 40) | (b1 << 48) | (b0 << 56);
 	}
+	
+	public String readString(int start, int size) throws IOException {
+		int sz = size + start - getPosition();
+		if (sz == 0) return null;
+		byte[] buf = new byte[sz];
+		int i = 0;
+		for (;; i++) {
+			int b = read();
+			if (b == 0) break;
+			buf[i] = (byte)b;
+		}
+		if (i == 0) return null;
+		return new String(buf, 0, i);
+	}
+	
+	public String readUnicodeString(int start, int size) throws IOException {
+		int sz = (size + start - getPosition())>>1;
+		if (sz == 0) return null;
+		char[] buf = new char[sz];		
+		int i = 0;
+		for (;; i++) {
+			char c = (char)read2bytes();
+			if (c == 0) break;
+			buf[i] = c;
+		}
+		if (i == 0) return null;
+		return new String(buf, 0, i);
+	}
 }
 
 enum Endianness {
diff --git a/src/io/ByteWriter.java b/src/io/ByteWriter.java
index ddbe31b..ad6ecf6 100644
--- a/src/io/ByteWriter.java
+++ b/src/io/ByteWriter.java
@@ -7,6 +7,8 @@ public class ByteWriter extends OutputStream {
 
 	private OutputStream stream;
 	private Endianness end = Endianness.LITTLE_ENDIAN;
+	private int pos = 0;
+	
 	
 	public ByteWriter(OutputStream out) {
 		stream = out;
@@ -38,8 +40,13 @@ public class ByteWriter extends OutputStream {
 		return end == Endianness.LITTLE_ENDIAN;
 	}
 	
+	public int getPosition() {
+		return pos;
+	}
+	
 	@Override
 	public void write(int b) throws IOException {
+		pos++;
 		stream.write(b);
 	}
 	
@@ -137,4 +144,9 @@ public class ByteWriter extends OutputStream {
 			write(b7); write(b6); write(b5); write(b4); write(b3); write(b2); write(b1); write(b0);
 		}
 	}
+	
+	public void writeBytes(byte[] b) throws IOException {
+		for (byte i : b) 
+			write(i);
+	}
 }
diff --git a/src/io/Bytes.java b/src/io/Bytes.java
index 4abadf9..b322dc8 100644
--- a/src/io/Bytes.java
+++ b/src/io/Bytes.java
@@ -39,12 +39,12 @@ public class Bytes {
 		return (Bytes.l(b7) << 56) | (Bytes.l(b6) << 48) | (Bytes.l(b5) << 40) | (Bytes.l(b4) << 32) | (Bytes.l(b3) << 24) | (Bytes.l(b2) << 16) | (Bytes.l(b1) << 8) | Bytes.l(b0);
 	}
 
-	static int i(byte b) {
-		return b & 0xff;
-	}
-
 	static long l(byte b) {
 		return b & 0xffL;
 	}
 
+	static int i(byte b) {
+		return b & 0xff;
+	}
+
 }
diff --git a/src/mslinks/LinkInfo.java b/src/mslinks/LinkInfo.java
new file mode 100644
index 0000000..abb3c05
--- /dev/null
+++ b/src/mslinks/LinkInfo.java
@@ -0,0 +1,232 @@
+package mslinks;
+
+import io.ByteReader;
+import io.ByteWriter;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetEncoder;
+
+import mslinks.data.*;
+
+public class LinkInfo implements Serializable {
+	private LinkInfoFlags lif;
+	private VolumeID vid;
+	private String localBasePath;
+	private CNRLink cnrlink;
+	private String commonPathSuffix;
+	
+	public LinkInfo() {
+		createVolumeID();
+	}
+	
+	public LinkInfo(ByteReader data) throws IOException, ShellLinkException {
+		int pos = data.getPosition();
+		int size = (int)data.read4bytes();
+		int hsize = (int)data.read4bytes();
+		lif = new LinkInfoFlags(data);
+		int vidoffset = (int)data.read4bytes();
+		int lbpoffset = (int)data.read4bytes();
+		int cnrloffset = (int)data.read4bytes();
+		int cpsoffset = (int)data.read4bytes();
+		int lbpoffset_u = 0, cpfoffset_u = 0;
+		if (hsize >= 0x24) {
+			lbpoffset_u = (int)data.read4bytes();
+			cpfoffset_u = (int)data.read4bytes();
+		}
+		
+		if (lif.hasVolumeIDAndLocalBasePath()) {
+			data.seek(pos + vidoffset - data.getPosition());
+			vid = new VolumeID(data);
+			data.seek(pos + lbpoffset - data.getPosition());
+			localBasePath = data.readString(pos, size);
+		}
+		if (lif.hasCommonNetworkRelativeLinkAndPathSuffix()) {
+			data.seek(pos + cnrloffset - data.getPosition());
+			cnrlink = new CNRLink(data);
+			data.seek(pos + cpsoffset - data.getPosition());
+			commonPathSuffix = data.readString(pos, size);
+		}
+		if (lif.hasVolumeIDAndLocalBasePath() && lbpoffset_u != 0) {
+			data.seek(pos + lbpoffset_u - data.getPosition());
+			localBasePath = data.readUnicodeString(pos, size);
+		}
+		if (lif.hasCommonNetworkRelativeLinkAndPathSuffix() && cpfoffset_u != 0) {
+			data.seek(pos + cpfoffset_u - data.getPosition());
+			commonPathSuffix = data.readUnicodeString(pos, size);
+		}
+		
+		data.seek(pos + size - data.getPosition());
+	}
+
+	public void serialize(ByteWriter bw) throws IOException {
+		int pos = bw.getPosition();
+		int hsize = 28;
+		CharsetEncoder ce = Charset.defaultCharset().newEncoder();
+		if (localBasePath != null && !ce.canEncode(localBasePath) || commonPathSuffix != null && !ce.canEncode(commonPathSuffix)) 
+			hsize += 8;
+		
+		byte[] vid_b = null, localBasePath_b = null, cnrlink_b = null, commonPathSuffix_b = null;
+		if (lif.hasVolumeIDAndLocalBasePath()) {
+			vid_b = toByteArray(vid, bw.isLitteEndian());
+			localBasePath_b = localBasePath.getBytes();
+			commonPathSuffix_b = new byte[0];
+		}
+		if (lif.hasCommonNetworkRelativeLinkAndPathSuffix()) {
+			cnrlink_b = toByteArray(cnrlink, bw.isLitteEndian());
+			commonPathSuffix_b = commonPathSuffix.getBytes();
+		}
+		
+		int size = hsize
+				+ (vid_b == null? 0 : vid_b.length)
+				+ (localBasePath_b == null? 0 : localBasePath_b.length + 1)
+				+ (cnrlink_b == null? 0 : cnrlink_b.length)
+				+ commonPathSuffix_b.length + 1;
+		
+		if (hsize > 28) {
+			if (lif.hasVolumeIDAndLocalBasePath()) {
+				size += localBasePath.length() * 2 + 2;
+				size += 1;
+			}
+			if (lif.hasCommonNetworkRelativeLinkAndPathSuffix()) {
+				size += commonPathSuffix.length() * 2 + 2;
+			} else 
+				size += 2;
+		}
+		
+		
+		bw.write4bytes(size);
+		bw.write4bytes(hsize);
+		lif.serialize(bw);
+		int off = hsize;
+		if (lif.hasVolumeIDAndLocalBasePath()) {
+			bw.write4bytes(off); // volumeid offset
+			off += vid_b.length;
+			bw.write4bytes(off); // localBasePath offset
+			off += localBasePath_b.length + 1;
+			bw.write4bytes(0); // CommonNetworkRelativeLinkOffset
+			bw.write4bytes(size - (hsize > 28 ? 4 : 1)); // fake commonPathSuffix offset 
+		}
+		if (lif.hasCommonNetworkRelativeLinkAndPathSuffix()) {
+			bw.write4bytes(0); // volumeid offset
+			bw.write4bytes(0); // localBasePath offset
+			bw.write4bytes(off); // CommonNetworkRelativeLink offset 
+			off += cnrlink_b.length;
+			bw.write4bytes(off); // commonPathSuffix
+			off += commonPathSuffix_b.length + 1;
+		}
+		if (hsize > 28) {
+			if (lif.hasVolumeIDAndLocalBasePath()) {
+				bw.write4bytes(off); // LocalBasePathOffsetUnicode
+				off += localBasePath.length() * 2 + 2;
+				bw.write4bytes(size - 2); // fake CommonPathSuffixUnicode offset
+			} else  {
+				bw.write4bytes(0);
+				bw.write4bytes(off); // CommonPathSuffixUnicode offset 
+				off += commonPathSuffix.length() * 2 + 2;
+			}				
+		}
+		
+		if (lif.hasVolumeIDAndLocalBasePath()) {
+			bw.writeBytes(vid_b);
+			bw.writeBytes(localBasePath_b);
+			bw.write(0);
+		}
+		if (lif.hasCommonNetworkRelativeLinkAndPathSuffix()) {
+			bw.writeBytes(cnrlink_b);
+			bw.writeBytes(commonPathSuffix_b);
+			bw.write(0);
+		}
+		
+		if (hsize > 28) {
+			if (lif.hasVolumeIDAndLocalBasePath()) {
+				for (int i=0; i<localBasePath.length(); i++)
+					bw.write2bytes(localBasePath.charAt(i));
+				bw.write2bytes(0);
+			}
+			if (lif.hasCommonNetworkRelativeLinkAndPathSuffix()) {
+				for (int i=0; i<commonPathSuffix.length(); i++)
+					bw.write2bytes(commonPathSuffix.charAt(i));
+				bw.write2bytes(0);
+			}
+		}
+		
+		while (bw.getPosition() < pos + size)
+			bw.write(0);		
+	}
+	
+	private byte[] toByteArray(Serializable o, boolean le) throws IOException {
+		ByteArrayOutputStream arr = new ByteArrayOutputStream();
+		ByteWriter bt = new ByteWriter(arr);
+		if (le) bt.setLittleEndian();
+		else bt.setBigEndian();
+		o.serialize(bt);
+		return arr.toByteArray();
+	}
+	
+	public VolumeID getVolumeID() { return vid; }
+	/**
+	 * Creates VolumeID and LocalBasePath that is empty string, clears CommonNetworkRelativeLink and CommonPathSuffix
+	 */
+	public VolumeID createVolumeID() {
+		cnrlink = null;
+		commonPathSuffix = null;
+		lif.clearCommonNetworkRelativeLinkAndPathSuffix();
+		
+		vid = new VolumeID();
+		localBasePath = "";
+		lif.setVolumeIDAndLocalBasePath();
+		return vid;
+	}
+	
+	public String getLocalBasePath() { return localBasePath; }
+	/**
+	 * Set LocalBasePath and creates new VolumeID (if it not exists), clears CommonNetworkRelativeLink and CommonPathSuffix.
+	 * If s is null takes no effect 
+	 */
+	public void setLocalBasePath(String s) {
+		if (s == null) return;
+		
+		localBasePath = s;
+		if (vid == null) vid = new VolumeID();
+		lif.setVolumeIDAndLocalBasePath();
+		
+		cnrlink = null;
+		commonPathSuffix = null;
+		lif.clearCommonNetworkRelativeLinkAndPathSuffix();
+	}
+	
+	public CNRLink getCommonNetworkRelativeLink() { return cnrlink; }
+	/**
+	 * Creates CommonNetworkRelativeLink and CommonPathSuffix that is empty string, clears VolumeID and LocalBasePath
+	 */
+	public CNRLink createCommonNetworkRelativeLink() {
+		cnrlink = new CNRLink();
+		commonPathSuffix = "";
+		lif.setCommonNetworkRelativeLinkAndPathSuffix();
+		
+		vid = null;
+		localBasePath = null;
+		lif.clearVolumeIDAndLocalBasePath();
+		
+		return cnrlink;
+	}
+	
+	public String getCommonPathSuffix() { return commonPathSuffix; }
+	/**
+	 * Set CommonPathSuffix and creates new CommonNetworkRelativeLink (if it not exists), clears VolumeID and LocalBasePath.
+	 * If s is null takes no effect 
+	 */
+	public void setCommonPathSuffix(String s) {
+		if (s == null) return;
+		
+		localBasePath = null;
+		vid = null;
+		lif.clearVolumeIDAndLocalBasePath();
+		
+		commonPathSuffix = s;
+		if (cnrlink == null) cnrlink = new CNRLink();		
+		lif.setCommonNetworkRelativeLinkAndPathSuffix();
+	}
+}
diff --git a/src/mslinks/LinkTargetIDList.java b/src/mslinks/LinkTargetIDList.java
index 4bd9f48..4dcd2ac 100644
--- a/src/mslinks/LinkTargetIDList.java
+++ b/src/mslinks/LinkTargetIDList.java
@@ -6,13 +6,13 @@ import io.ByteWriter;
 import java.io.IOException;
 import java.util.LinkedList;
 
-public class LinkTargetIDList {
+public class LinkTargetIDList implements Serializable {
 	private LinkedList<byte[]> list = new LinkedList<>();
 	
 	public LinkTargetIDList(ByteReader data) throws IOException, ShellLinkException {
 		int size = (int)data.read2bytes();
 		
-		int check = data.getPosition(); 
+		int pos = data.getPosition(); 
 		
 		int s = (int)data.read2bytes();
 		while (s != 0) {
@@ -24,8 +24,8 @@ public class LinkTargetIDList {
 			s = (int)data.read2bytes();
 		}
 		
-		check = data.getPosition() - check;
-		if (check != size) 
+		pos = data.getPosition() - pos;
+		if (pos != size) 
 			throw new ShellLinkException();
 	}
 
diff --git a/src/mslinks/Main.java b/src/mslinks/Main.java
index 4395b58..1fed2fd 100644
--- a/src/mslinks/Main.java
+++ b/src/mslinks/Main.java
@@ -9,8 +9,12 @@ import mslinks.data.Filetime;
 
 public class Main {
 	public static void main(String[] args) throws IOException, ShellLinkException {
-		ShellLink link = new ShellLink("testlink.lnk");
-		Filetime ft = link.getWriteTime();
+		//for (String i : Charset.availableCharsets().keySet())
+		//	System.out.println(i);
+		//if (true) return;
+		
+		ShellLink link = new ShellLink("testlink3.lnk");
+		Filetime ft = link.getHeader().getWriteTime();
 		System.out.println(String.format("%d:%d:%d %d.%d.%d", ft.get(GregorianCalendar.HOUR_OF_DAY), ft.get(GregorianCalendar.MINUTE), ft.get(GregorianCalendar.SECOND),
 				ft.get(GregorianCalendar.DAY_OF_MONTH), ft.get(GregorianCalendar.MONTH) + 1, ft.get(GregorianCalendar.YEAR)));
 		
diff --git a/src/mslinks/Serializable.java b/src/mslinks/Serializable.java
new file mode 100644
index 0000000..ec68dd5
--- /dev/null
+++ b/src/mslinks/Serializable.java
@@ -0,0 +1,9 @@
+package mslinks;
+
+import java.io.IOException;
+
+import io.ByteWriter;
+
+public interface Serializable {
+	void serialize(ByteWriter bw) throws IOException;
+}
diff --git a/src/mslinks/ShellLink.java b/src/mslinks/ShellLink.java
index d720fe4..87e71ce 100644
--- a/src/mslinks/ShellLink.java
+++ b/src/mslinks/ShellLink.java
@@ -8,16 +8,12 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 
-import mslinks.data.FileAttributesFlags;
-import mslinks.data.Filetime;
-import mslinks.data.HotKeyFlags;
-import mslinks.data.LinkFlags;
-
 public class ShellLink {
 	
 	private boolean le;
 	private ShellLinkHeader header;
 	private LinkTargetIDList idlist;
+	private LinkInfo info;
 	
 	public ShellLink(String file) throws IOException, ShellLinkException {
 		this(Paths.get(file));
@@ -39,6 +35,8 @@ public class ShellLink {
 		header = new ShellLinkHeader(data);
 		if (header.getLinkFlags().hasLinkTargetIDList()) 
 			idlist = new LinkTargetIDList(data);
+		if (header.getLinkFlags().hasLinkInfo())
+			info = new LinkInfo(data);
 		le = data.isLitteEndian();
 	}
 		
@@ -47,23 +45,17 @@ public class ShellLink {
 		if (le) bw.setLittleEndian();
 		else bw.setBigEndian();
 		header.serialize(bw);
-		idlist.serialize(bw);
+		if (header.getLinkFlags().hasLinkTargetIDList())
+			idlist.serialize(bw);
+		if (header.getLinkFlags().hasLinkInfo())
+			info.serialize(bw);
 	}
 	
-	/*    to header      */
-	public LinkFlags getLinkFlags() { return header.getLinkFlags(); }
-	public FileAttributesFlags getFileAttributesFlags() { return header.getFileAttributesFlags(); }
-	public Filetime getCreationTime() { return header.getCreationTime(); }
-	public Filetime getAccessTime() { return header.getAccessTime(); }
-	public Filetime getWriteTime() { return header.getWriteTime(); }
-	public HotKeyFlags getHotKeyFlags() { return header.getHotKeyFlags(); }
-	
-	public int getFileSize() { return header.getFileSize(); }
-	public void setFileSize(long n) { header.setFileSize(n); }
+	public ShellLinkHeader getHeader() { return header; }
 	
-	public int getIconIndex() { return header.getIconIndex(); }
-	public void setIconIndex(int n) { header.setIconIndex(n); }
-	
-	public int getShowCommand() { return header.getShowCommand(); }
-	public void setShowCommand(int n) throws ShellLinkException { header.setShowCommand(n); }
+	public LinkInfo getLinkInfo() { return info; }
+	public void createLinkInfo() {
+		info = new LinkInfo();
+		header.getLinkFlags().setHasLinkInfo();
+	}
 }
diff --git a/src/mslinks/ShellLinkHeader.java b/src/mslinks/ShellLinkHeader.java
index 673df7f..2655ad3 100644
--- a/src/mslinks/ShellLinkHeader.java
+++ b/src/mslinks/ShellLinkHeader.java
@@ -12,7 +12,7 @@ import mslinks.data.GUID;
 import mslinks.data.HotKeyFlags;
 import mslinks.data.LinkFlags;
 
-public class ShellLinkHeader {
+public class ShellLinkHeader implements Serializable {
 	private static byte b(int i) { return (byte)i; }
 	private static int headerSize = 0x0000004C;
 	private static GUID clsid = new GUID(new byte[] {
@@ -22,9 +22,9 @@ public class ShellLinkHeader {
 			b(0xc0), b(0x00),  
 			b(0x00),  b(0x00),  b(0x00),  b(0x00),  b(0x00),  b(0x46) });
 	
-	public static int SW_SHOWNORMAL = 1;
-	public static int SW_SHOWMAXIMIZED = 3;
-	public static int SW_SHOWMINNOACTIVE = 7;	
+	public static final int SW_SHOWNORMAL = 1;
+	public static final int SW_SHOWMAXIMIZED = 3;
+	public static final int SW_SHOWMINNOACTIVE = 7;	
 	
 	private LinkFlags lf;
 	private FileAttributesFlags faf;
@@ -54,6 +54,8 @@ public class ShellLinkHeader {
 		fileSize = (int)data.read4bytes();
 		iconIndex = (int)data.read4bytes();
 		showCommand = (int)data.read4bytes();
+		if (showCommand != SW_SHOWNORMAL && showCommand != SW_SHOWMAXIMIZED && showCommand != SW_SHOWMINNOACTIVE)
+			throw new ShellLinkException();
 		hkf = new HotKeyFlags(data);
 		data.read2bytes();
 		data.read8bytes();
diff --git a/src/mslinks/data/BitSet32.java b/src/mslinks/data/BitSet32.java
index ae7bbc9..89ae3b2 100644
--- a/src/mslinks/data/BitSet32.java
+++ b/src/mslinks/data/BitSet32.java
@@ -5,7 +5,9 @@ import io.ByteWriter;
 
 import java.io.IOException;
 
-public class BitSet32 {
+import mslinks.Serializable;
+
+public class BitSet32 implements Serializable {
 	private int d;
 	
 	public BitSet32(int n) {
diff --git a/src/mslinks/data/CNRLink.java b/src/mslinks/data/CNRLink.java
new file mode 100644
index 0000000..546e991
--- /dev/null
+++ b/src/mslinks/data/CNRLink.java
@@ -0,0 +1,201 @@
+package mslinks.data;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetEncoder;
+
+import mslinks.Serializable;
+import mslinks.ShellLinkException;
+import io.ByteReader;
+import io.ByteWriter;
+
+public class CNRLink implements Serializable {
+	
+	public static final int WNNC_NET_AVID = 0x001A000;
+	public static final int WNNC_NET_DOCUSPACE = 0x001B000;
+	public static final int WNNC_NET_MANGOSOFT = 0x001C000;
+	public static final int WNNC_NET_SERNET = 0x001D000;
+	public static final int WNNC_NET_RIVERFRONT1 = 0X001E000;
+	public static final int WNNC_NET_RIVERFRONT2 = 0x001F000;
+	public static final int WNNC_NET_DECORB = 0x0020000;
+	public static final int WNNC_NET_PROTSTOR = 0x0021000;
+	public static final int WNNC_NET_FJ_REDIR = 0x0022000;
+	public static final int WNNC_NET_DISTINCT = 0x0023000;
+	public static final int WNNC_NET_TWINS = 0x0024000;
+	public static final int WNNC_NET_RDR2SAMPLE = 0x0025000;
+	public static final int WNNC_NET_CSC = 0x0026000;
+	public static final int WNNC_NET_3IN1 = 0x0027000;
+	public static final int WNNC_NET_EXTENDNET = 0x0029000;
+	public static final int WNNC_NET_STAC = 0x002A000;
+	public static final int WNNC_NET_FOXBAT = 0x002B000;
+	public static final int WNNC_NET_YAHOO = 0x002C000;
+	public static final int WNNC_NET_EXIFS = 0x002D000;
+	public static final int WNNC_NET_DAV = 0x002E000;
+	public static final int WNNC_NET_KNOWARE = 0x002F000;
+	public static final int WNNC_NET_OBJECT_DIRE = 0x0030000;
+	public static final int WNNC_NET_MASFAX = 0x0031000;
+	public static final int WNNC_NET_HOB_NFS = 0x0032000;
+	public static final int WNNC_NET_SHIVA = 0x0033000;
+	public static final int WNNC_NET_IBMAL = 0x0034000;
+	public static final int WNNC_NET_LOCK = 0x0035000;
+	public static final int WNNC_NET_TERMSRV = 0x0036000;
+	public static final int WNNC_NET_SRT = 0x0037000;
+	public static final int WNNC_NET_QUINCY = 0x0038000;
+	public static final int WNNC_NET_OPENAFS = 0x0039000;
+	public static final int WNNC_NET_AVID1 = 0X003A000;
+	public static final int WNNC_NET_DFS = 0x003B000;
+	public static final int WNNC_NET_KWNP = 0x003C000;
+	public static final int WNNC_NET_ZENWORKS = 0x003D000;
+	public static final int WNNC_NET_DRIVEONWEB = 0x003E000;
+	public static final int WNNC_NET_VMWARE = 0x003F000;
+	public static final int WNNC_NET_RSFX = 0x0040000;
+	public static final int WNNC_NET_MFILES = 0x0041000;
+	public static final int WNNC_NET_MS_NFS = 0x0042000;
+	public static final int WNNC_NET_GOOGLE = 0x0043000;
+	
+	private CNRLinkFlags flags; 
+	private int nptype;
+	private String netname, devname;
+
+	public CNRLink() {
+		netname = "";
+	}
+	
+	public CNRLink(ByteReader data) throws ShellLinkException, IOException {
+		int pos = data.getPosition();
+		int size = (int)data.read4bytes();
+		if (size < 0x14)
+			throw new ShellLinkException();
+		flags = new CNRLinkFlags(data);
+		int nnoffset = (int)data.read4bytes();
+		int dnoffset = (int)data.read4bytes();
+		if (!flags.isValidDevice())
+			dnoffset = 0;
+		nptype = (int)data.read4bytes();
+		if (flags.isValidNetType())
+			checkNptype(nptype);
+		else nptype = 0;
+		
+		int nnoffset_u = 0, dnoffset_u = 0;
+		if (nnoffset > 0x14) {
+			nnoffset_u = (int)data.read4bytes();
+			dnoffset_u = (int)data.read4bytes();
+		}
+		
+		data.seek(pos + nnoffset - data.getPosition());
+		netname = data.readString( pos, size);
+		if (dnoffset != 0) {
+			data.seek(pos + dnoffset - data.getPosition());
+			devname = data.readString(pos, size);
+		}
+		if (nnoffset_u != 0) netname = data.readUnicodeString(pos, size);
+		if (dnoffset_u != 0) devname = data.readUnicodeString(pos, size);
+	}
+	
+	private void checkNptype(int type) throws ShellLinkException {
+		int mod = Modifier.PUBLIC | Modifier.STATIC | Modifier.FINAL;
+		for (Field f : this.getClass().getFields()) {
+			try {
+				if ((f.getModifiers() & mod) == mod && type == ((Integer)f.get(null)).intValue())
+					return;
+			} catch (Exception e) {}
+		}
+		throw new ShellLinkException("incorrect network type");
+	}
+
+	@Override
+	public void serialize(ByteWriter bw) throws IOException {
+		int size = 20;
+		boolean u = false;
+		CharsetEncoder ce = Charset.defaultCharset().newEncoder();
+		u = !ce.canEncode(netname) || devname != null && !ce.canEncode(devname);
+		
+		if (u) size += 8;
+		byte[] netname_b = null, devname_b = null;
+		netname_b = netname.getBytes();
+		if (devname != null) devname_b = devname.getBytes();
+		size += netname_b.length + 1;
+		if (devname_b != null) size += devname_b.length + 1;
+		
+		if (u) {
+			size += netname.length() * 2 + 2;
+			if (devname != null) size += devname.length() * 2 + 2;
+		}
+		
+		bw.write4bytes(size);
+		flags.serialize(bw);
+		int off = 20;
+		if (u) off += 8;
+		bw.write4bytes(off); // netname offset
+		off += netname_b.length + 1;
+		if (devname_b != null) {
+			bw.write4bytes(off); // devname offset
+			off += devname_b.length + 1;
+		} else bw.write4bytes(0);
+		bw.write4bytes(nptype);
+		if (u) {
+			bw.write4bytes(off);
+			off += netname.length() * 2 + 2;
+			if (devname != null) {
+				bw.write4bytes(off);
+				off += devname.length() * 2 + 2;
+			} else bw.write4bytes(0);
+		}
+		bw.writeBytes(netname_b);
+		bw.write(0);
+		if (devname_b != null) {
+			bw.writeBytes(devname_b);
+			bw.write(0);
+		}
+		if (u) {
+			for (int i=0; i<netname.length(); i++)
+				bw.write2bytes(netname.charAt(i));
+			bw.write2bytes(0);
+			if (devname != null) {
+				for (int i=0; i<devname.length(); i++)
+					bw.write2bytes(devname.charAt(i));
+				bw.write2bytes(0);
+			}
+		}
+	}
+	
+	public int getNetworkType() { return nptype; }
+	/**
+	 * pass zero to switch off network type
+	 */
+	public void setNetworlType(int n) throws ShellLinkException {
+		if (n == 0) {
+			flags.clearValidNetType();
+			nptype = n;
+		} else {
+			checkNptype(n);
+			flags.setValidNetType();
+			nptype = n;
+		}
+	}
+	
+	public String getNetName() { return netname; }
+	/** 
+	 * if s is null take no effect
+	 */
+	public void setNetName(String s) throws ShellLinkException {
+		if (s == null) return;
+		netname = s; 
+	}
+	
+	public String getDeviceName() { return devname; }
+	/**
+	 * pass null to switch off device info
+	 */
+	public void setDeviceName(String s) {
+		if (s == null) {
+			devname = null;
+			flags.clearValidDevice();
+		} else {
+			devname = s;
+			flags.setValidDevice();
+		}
+	}
+}
diff --git a/src/mslinks/data/CNRLinkFlags.java b/src/mslinks/data/CNRLinkFlags.java
new file mode 100644
index 0000000..69438d2
--- /dev/null
+++ b/src/mslinks/data/CNRLinkFlags.java
@@ -0,0 +1,33 @@
+package mslinks.data;
+
+import io.ByteReader;
+
+import java.io.IOException;
+
+public class CNRLinkFlags extends BitSet32 {
+	
+	public CNRLinkFlags(int n) {
+		super(n);
+		reset();
+	}
+
+	public CNRLinkFlags(ByteReader data) throws IOException {
+		super(data);
+		reset();
+	}
+	
+	private void reset() {
+		for (int i=2; i<32; i++)
+			clear(i);
+	}
+	
+	public boolean isValidDevice() 		{ return get(0); }
+	public boolean isValidNetType()		{ return get(1); }
+	
+	public void setValidDevice() 		{ set(0); }	
+	public void setValidNetType()		{ set(1); }
+	
+	public void clearValidDevice() 		{ clear(0); }	
+	public void clearValidNetType()		{ clear(1); }
+
+}
diff --git a/src/mslinks/data/FileAttributesFlags.java b/src/mslinks/data/FileAttributesFlags.java
index d8fb166..533c139 100644
--- a/src/mslinks/data/FileAttributesFlags.java
+++ b/src/mslinks/data/FileAttributesFlags.java
@@ -7,10 +7,19 @@ import java.io.IOException;
 public class FileAttributesFlags extends BitSet32 {
 	public FileAttributesFlags(int n) {
 		super(n);
+		reset();
 	}
 	
 	public FileAttributesFlags(ByteReader data) throws IOException {
 		super(data);
+		reset();
+	}
+	
+	private void reset() {
+		clear(3);
+		clear(6);
+		for (int i=15; i<32; i++)
+			clear(i);
 	}
 	
 	public boolean isReadonly() 			{ return get(0); }
diff --git a/src/mslinks/data/Filetime.java b/src/mslinks/data/Filetime.java
index 3fc824f..2d21636 100644
--- a/src/mslinks/data/Filetime.java
+++ b/src/mslinks/data/Filetime.java
@@ -6,7 +6,9 @@ import io.ByteWriter;
 import java.io.IOException;
 import java.util.GregorianCalendar;
 
-public class Filetime extends GregorianCalendar {
+import mslinks.Serializable;
+
+public class Filetime extends GregorianCalendar implements Serializable {
 	private long residue;
 	
 	public Filetime(ByteReader data) throws IOException {
diff --git a/src/mslinks/data/GUID.java b/src/mslinks/data/GUID.java
index 4c820a0..c3e65a3 100644
--- a/src/mslinks/data/GUID.java
+++ b/src/mslinks/data/GUID.java
@@ -6,7 +6,9 @@ import io.Bytes;
 
 import java.io.IOException;
 
-public class GUID {
+import mslinks.Serializable;
+
+public class GUID implements Serializable {
 	private int d1;
 	private short d2, d3, d4;
 	private long d5;
diff --git a/src/mslinks/data/HotKeyFlags.java b/src/mslinks/data/HotKeyFlags.java
index 6c780aa..c919094 100644
--- a/src/mslinks/data/HotKeyFlags.java
+++ b/src/mslinks/data/HotKeyFlags.java
@@ -6,7 +6,9 @@ import io.ByteWriter;
 import java.io.IOException;
 import java.util.HashMap;
 
-public class HotKeyFlags {
+import mslinks.Serializable;
+
+public class HotKeyFlags implements Serializable {
 	private static HashMap<Byte, String> keys = new HashMap<Byte, String>() {{
 		put((byte)0x30, "0");
 		put((byte)0x31, "1");
diff --git a/src/mslinks/data/LinkFlags.java b/src/mslinks/data/LinkFlags.java
index c565cbd..fd65096 100644
--- a/src/mslinks/data/LinkFlags.java
+++ b/src/mslinks/data/LinkFlags.java
@@ -8,10 +8,19 @@ public class LinkFlags extends BitSet32 {
 	
 	public LinkFlags(int n) {
 		super(n);
+		reset();
 	}
 	
 	public LinkFlags(ByteReader data) throws IOException {
 		super(data);
+		reset();
+	}
+	
+	private void reset() {
+		clear(11);
+		clear(16);
+		for (int i=27; i<32; i++)
+			clear(i);
 	}
 	
 	public boolean hasLinkTargetIDList() 			{ return get(0); }	
diff --git a/src/mslinks/data/LinkInfoFlags.java b/src/mslinks/data/LinkInfoFlags.java
new file mode 100644
index 0000000..3514c68
--- /dev/null
+++ b/src/mslinks/data/LinkInfoFlags.java
@@ -0,0 +1,32 @@
+package mslinks.data;
+
+import io.ByteReader;
+
+import java.io.IOException;
+
+public class LinkInfoFlags extends BitSet32 {
+	
+	public LinkInfoFlags(int n) {
+		super(n);
+		reset();
+	}
+	
+	public LinkInfoFlags(ByteReader data) throws IOException {
+		super(data);
+		reset();
+	}
+	
+	private void reset() {
+		for (int i=2; i<32; i++)
+			clear(i);
+	}
+	
+	public boolean hasVolumeIDAndLocalBasePath() 				{ return get(0); }
+	public boolean hasCommonNetworkRelativeLinkAndPathSuffix()	{ return get(1); }
+	
+	public void setVolumeIDAndLocalBasePath() 					{ set(0); }	
+	public void setCommonNetworkRelativeLinkAndPathSuffix()		{ set(1); }
+	
+	public void clearVolumeIDAndLocalBasePath() 				{ clear(0); }	
+	public void clearCommonNetworkRelativeLinkAndPathSuffix()	{ clear(1); }
+}
diff --git a/src/mslinks/data/VolumeID.java b/src/mslinks/data/VolumeID.java
new file mode 100644
index 0000000..f186d78
--- /dev/null
+++ b/src/mslinks/data/VolumeID.java
@@ -0,0 +1,125 @@
+package mslinks.data;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+
+import mslinks.Serializable;
+import mslinks.ShellLinkException;
+import io.ByteReader;
+import io.ByteWriter;
+
+public class VolumeID implements Serializable {
+	public static final int DRIVE_UNKNOWN = 0;
+	public static final int DRIVE_NO_ROOT_DIR = 1;
+	public static final int DRIVE_REMOVABLE = 2;
+	public static final int DRIVE_FIXED = 3;
+	public static final int DRIVE_REMOTE = 4;
+	public static final int DRIVE_CDROM = 5;
+	public static final int DRIVE_RAMDISK = 6;
+	
+	
+	private int dt;
+	private int dsn;
+	private String label;
+
+	public VolumeID() {
+		dt = DRIVE_UNKNOWN;
+		dsn = (int)(Math.random() * Long.MAX_VALUE);
+		label = "";
+	}
+	
+	public VolumeID(ByteReader data) throws ShellLinkException, IOException {
+		int pos = data.getPosition();
+		int size = (int)data.read4bytes();
+		if (size <= 0x10)
+			throw new ShellLinkException();
+		
+		dt = (int)data.read4bytes();
+		if (dt != DRIVE_NO_ROOT_DIR && dt != DRIVE_REMOVABLE && dt != DRIVE_FIXED 
+				&& dt != DRIVE_REMOTE && dt != DRIVE_CDROM && dt != DRIVE_RAMDISK)
+			dt = DRIVE_UNKNOWN;
+		dsn = (int)data.read4bytes();
+		int vloffset = (int)data.read4bytes();
+		boolean u = false;
+		if (vloffset == 0x14) {
+			vloffset = (int)data.read4bytes();
+			u = true;
+		}
+
+		data.seek(pos + vloffset - data.getPosition());
+		
+		int i=0;
+		if (u) {
+			char[] buf = new char[(size-vloffset)>>1];
+			for (;; i++) {
+				char c = (char)data.read2bytes();
+				if (c == 0) break;
+				buf[i] = c;
+			}
+			label = new String(buf, 0, i);
+		} else {
+			byte[] buf = new byte[size-vloffset];
+			for (;; i++) {
+				int b = data.read();
+				if (b == 0) break;
+				buf[i] = (byte)b;
+			}
+			label = new String(buf, 0, i);
+		}
+	}
+	
+	public void serialize(ByteWriter bw) throws IOException {
+		int size = 16;
+		byte[] label_b = label.getBytes();
+		size += label_b.length + 1;
+		boolean u = false;
+		if (!Charset.defaultCharset().newEncoder().canEncode(label)) { 
+			size += 4 + 1 + label.length() * 2 + 2;
+			u = true;
+		}
+		
+		bw.write4bytes(size);
+		bw.write4bytes(dt);
+		bw.write4bytes(dsn);
+		int off = 16;
+		if (u) off += 4;
+		bw.write4bytes(off);
+		off += label_b.length + 1;		
+		if (u) {
+			off++;
+			bw.write4bytes(off);
+			off += label.length() * 2 + 2;
+		}
+		
+		bw.writeBytes(label_b);
+		bw.write(0);
+		if (u) {
+			bw.write(0);
+			for (int i=0; i<label.length(); i++)
+				bw.write2bytes(label.charAt(i));
+			bw.write2bytes(0);
+		}
+	}
+	
+	public int getDriveType() { return dt;}
+	public void setDriveType(int n) throws ShellLinkException {
+		if (n == DRIVE_UNKNOWN || n == DRIVE_NO_ROOT_DIR || n == DRIVE_REMOVABLE || n == DRIVE_FIXED 
+				|| n == DRIVE_REMOTE || n == DRIVE_CDROM || n == DRIVE_RAMDISK)
+			dt = n;
+		else 
+			throw new ShellLinkException("incorrect drive type");
+	}
+	
+	public int getSerialNumber() { return dsn; }
+	public void setSerialNumber(int n) { dsn = n; }
+	
+	public String getLabel() { return label; }
+	/** 
+	 * if s is null take no effect
+	 */
+	public void setLabel(String s) {
+		if (s == null) return;
+		label = s;
+	}
+
+}
diff --git a/test.lnk b/test.lnk
index 7dea24b522b7019a5816e696f1463e6a2c8821ab..d8688532def8dbb7b23430c8e5233522249c54fa 100644
GIT binary patch
literal 923
zcmbu7!D|yi6vn^QReBL37O_z1fFg;Sb+)0b3D%TuBWpBGp)DBLgAvzeS+iRSX?v>w
zfP%ChytD`Z0WX3li{MqM-aO_Y2oVwVpdf<Z%-YsN5X4>PeQ(~(o8Qcvxe6dPG(Zcy
z&=!|AFbF~a<+o=??;qSr?dqqAQ(sj7=c=b?rjq%4_VB<EMrXUVH=<Vz(vRN_;*6A=
z!#GziCJB2pwX+IK>-hAc+7XP8Io28jHwZWIdAl-I*;FH^Ip@xTLha+u>%0-eF>K?C
zQ8c6hOrnh@eB9=r#~4i9)c#rI*t1}x0Li>#BqZD|=gCGI_1eN*`LZHcev?&PHAS9U
zR@9_C8mdhlF*@Y=2p}-8HDoMg6gZCElO+VJP_T1VMY!IF@brONegA9;#o}%Y+_T{B
z1*%-T#U>DG>!Lx=XFmg{8Oi5&vzmL4CsmiT<t#rGkhWW!bJr}hI-3WU`mlHUvL(4D
z*LKNLHC1Pfw4EVyk!%%N(L2dt^j0XZJ(`y5GRv$MIfP+^Z88O%q*Am_P}O&VfpzLd
z>M->H+xOSK?KJgm?_>A9IAMWL=GE5^KYCrS9(US)Am;rbjKhwnswr}!8n<E*#lmew
zzQ~kHrK~4yQ@C-v8HgwlGyY;D5G%1B<EYtM%A<@TJpB1?JpAo0J-(EGy$6r4?EmY>
GQs5VnOs+ft

literal 559
zcmZ9Jy-UMD7{;FzhdL-xi&zls;-d6RP--cn?a_qN(vpZER<vp>A^k1{x^!|c6m(G+
zC;tM$MFf|E;0G>_x``nE3%+-TMohTpo_p_kf0uA+0FuW=3fu}s@)U0P$lo>gx})Al
zY51rtX53$D%JYL%Q)fj|`ppmYUGiWi_m+L~eMSxW;?<4$Q0Ux?pc>5&LXO!$SS%LV
z@$MLOv(1`@RRmV(_#u4X#Nw`XzBfmyE0G*`nuXJ0q78;73|OF6Od)Smn+f$qMo!Br
z+FFMWVm9?-b^na-acW0UmW4V}CteMQqy<#yHBhA$XQym-kX`p=qzYC6ShBg%c+bV_
zockhRZU-q`ig*?czyr-DMc7DPMx|9Ys%8r@o5ibqa8_6R^u#|_1uc}tmzm^%NgQAS
z{9-l}#h?#68Qrg>JK;c%B>{E7?KdK<-{9P@X^xb+{CT#Jfet?uq>&}JjR5MzE>VUz
F`~Z-wTbcj>

diff --git a/testlink.lnk b/testlink.lnk
index 869c5c67a2db6a48a8d98ab8bb9fa422890fed37..edce87d281ba6ff62ee8225626bf5fdfe21a1661 100644
GIT binary patch
delta 94
zcmX@f^^9wSK2u;D0|SE$5HmvQARr9_tw3xB#LPf^;L*G)5g@Gq#P%0oy?*dSK|$G=
efx*c@k;RI|n#F*{ghc@;tIPlt+?>qxj0pe$;}Hk|

delta 65
zcmaFHb&_j?K9fl%0|SE$5Hmt)Qy>ij89*!v#LPf^;L*G)0U&+x)$0dO6qJoOH!wY8
F0svo34f_B9

diff --git a/testlink2.lnk b/testlink2.lnk
new file mode 100644
index 0000000000000000000000000000000000000000..a28907e3c32664b53979dc7f2d65c3c925abf501
GIT binary patch
literal 1325
zcmd5*Ur1A76hG656;TtmQmMQkmX)o0B})f}_ijvRTN{bPKa*Ux()Q=Z<`sR&td|u8
z1!5FsXiyLd8dOF>?xhDIX+=a5m1MnWWj^)V?_6DpJ@o1azw_^&d%oXy&iA>A$Y9Qb
zBueR$oV{cu0rsZh+K&wb6NZdVCa-V%3IFrg^kz;quvoIH4Q49vO?lJ8jIGUd>z$Ey
ziDG{q?Qz-yOVTo?n<U+|Idb-!NRG8dLsX8fSJB+F19GyR<2#X2sAnzTOr{vMQItaX
zYNi)dN=!?UcRUivRqOE{@^(vBkIPRqvRG4*EyWD3`E4T?HDIr$%S9S0syKX1mmc_G
zSQ?ceR!>2A8YK{6oyk%VPf)A=M<XfZpg34Xk<p19i7LQNge^gq0|Lq9Cp)y0+_3G`
zL{{X7sRLxVvu78zVNsVs^XdWefNbFVSo((v`qj+r)EA+!l+a|ohXI!sRfU);c(kyp
zl?e60u$EB68i$l%qE!_d6-PW2)j9>KqEzgZ#Bx!P{dU3WmhDZ}Xc#Pvpk|{0$Yq*5
zh%2&&Xka%uNj1J2alUADt~%dzdp@iA;?3EiK2TW;PywI-E1{Q_;Gs{LNm&3NhSUxj
z!N|HWq&RtSiTI5BXfIAI{29YS{s#O*RV=y^pZF<ok#+2|!55Cpv%MZz$h>!XJ3kWk
zWq_OIuA7=ZKiY8csc@#jF+TL62=UdNz`W4af4a@zc<}?_0uS2X|4J&^^6-=S`;`;g
zOWyGqCsYAFVx+sUXI$U$Ieh-%&dHYI%D&e*58qPCJDHoli}+SR-_ZrVe7=DdF)(KM
z_tb-19-GSylIO}~WNiFI{xv@J;cAE~ATtp*c{Kxg7Xx+dU>%JFTf%BbC>~Mc+^RRZ
l@e8N=Pb*`VT1)h}^~lv5{WZPkrgJWhkk6kY<Nv;M&EF9Z39kSE

literal 0
HcmV?d00001

diff --git a/testlink3.lnk b/testlink3.lnk
new file mode 100644
index 0000000000000000000000000000000000000000..35f78d76f470af829aec901d3b277c2ef0247e07
GIT binary patch
literal 2321
zcmc&#Z%kWN6hCEeTK^2&fXg6@y`oM~TVH{$v>Ag@Iyxw8lMVt&P3T59YH1&%!loal
zG1&}m#yO_j2VwaCg9~cZpiYp)#7}NxGa67o;G8jbL9&_XjGFj6_qD(WQe8B9U(P+}
zo_pT?oqO)N=lY4rP?(3FXj1mHd60}GAb<SJSiw^(&l<9Pu6o@S9{;g7EG-SX{X)Mg
zuaGRk*}#-YyJMyor;T*4sty&={a&~J!-Az0_L5z4NBwmHGdapV_I*%n2e?A>6ODC^
zLp*a2Y<9OW)FND!{BNz|QJhMX;#O3JsFqUHMN#U;S3%`uqfRN`M^=pN<RllV_-{23
zblhRj8-pfl54425TNt@9gUos+My~5;R6RTrRCev8Q6KQ46eB^~B#%QEhn8p!Wp}0{
z*j+BCwV4q*mO*&uFt5J2Hyy<eZ(Z<Nz<WW>SUUj{l$5=XB9NjOmy)L|sg6HdCUFm8
zr+hfWp8lhPRHrWx^6j(Rnu8TYJ<DL9T^3sft_^GZfW<N8Jtk$p6_~q#&6+*ZlTxzm
zjH_+O>(wJ_8U9KD2Vk@oRBIr*6C6zPYH)5Fpg01)5xfX|1?cMO^n?lgT>8@NMRA9n
zkTReC^!;z?*>Jnomx{*3wrDJ_#rwh>O<@(yT0#>^P52_os3@(esVNJqPMh#)sjiqv
z#>Cd>u1HMm(c~OvZHb-=@{*gvv@~zRl<O?R$dqe6hcL42pT`fOz*+tm{yv7ga|6-H
z4?whsf+6+tU1ReN#imy-=XD$!zI3t$`Wx9D3#bJ2Z*0$nb%3icS_r<0wjj%c$Q?Iv
zz6Y`Q21tHn+r!AZUdWtRw}Nf~;ku*UJ}>JND{tm83)$ktay~0-xudUS<zneQ5O37x
ze5v*MGw01G@~Pw4CkK8uL%)&@3P7i?C!G8eCs~>68rBehd<}UsnUY~aN*gd(4a${g
ze2?`7^d;C8IH0>C1;_&+_WQz<ZJR&zS9wnTUNE@sc;ZX~`h{59FbJV$*g=j=<>UIt
zwZs7}OXZL&rA-yMi>hS3tVC~>QaO%zJ+y4;K+mhG^26e2RuDUKvFPOJ=z!7t{@AGl
zk4#*cW3RcE(M{ZCS_GVpXZN4D@@t^W^!hV+74Cj}#tXeP=CmpX{k>|FjZ!w$sL&aJ
z!%BVULH{}P!5$;6f2()}dn3cbCr3Z#$>iWtBZXA_q32lU_5z4qoKwfY^w2=VYgeZV
zUQX>S*$;gu8?cL<4A))^_+Yb_c%u?f1Z`OIXyq<GdM+{k3}eH=S0CI_TK(*K^SfWr
zWMF2^)Hw9Fu>m{S0?I-yzKgjb{@?n>p7Wb1Ux1M|9-wXSj_inQ{n6fTt(T)e{lREc
z>(}4-M>bi~5A=Tb(Gx>Mp~1($H9!3(1zRV{_*e7%s^D6WtOBnD@ymn+9ohZ)bW>5{
RGvCCqx)Nc`_wXFIKLLy(s#gF2

literal 0
HcmV?d00001

diff --git a/testlink4.lnk b/testlink4.lnk
new file mode 100644
index 0000000000000000000000000000000000000000..84f15f7bc5b3c09d830897392de8088e4d84aa20
GIT binary patch
literal 2184
zcmds2Z%9*76hG5PEz5GWN=D{0Qc9d`Z&+d)+n#3G=7uW~d66=8YNOdUv%VB0nP4C#
z{`+7VR1j(^7Dkc|ionQfeF)V4NESg6Dt+jKft~ZV=}@7NK6xI$^Ugi*+;h*l_uX?0
z03eZvAP4Y>91dO!kpfF{ZbQxN+)K&g_qMpTqNPOS3+xz*+Yvr*K0S@~h&aDq?o*2+
zK`@wjo#Bwla#Zj>_l%4Kr&4Q_YEH>%1eHpiSYBa6hXOu!aDWvG@m-3N3*4xSVNw_s
zlg1oj1mHjgYS4fZIftA>S&wn718BdCi>WPUi)tpee=1WEaeF4D;L7#yoiV5nqi~`T
zQiMu$k_hEc0X7^f5ez~!2*)?eBr`gO$2AY&+AL&&88RS~OkfLQ6A4;k{+%P`1{#wP
z*fx?kl2S`7u*WDKMo^&jOnOnB!7Hn)kiy<xMJUBV%ZqW)%y^cABz!nLrn3kQ%4!Tg
zfh8+|G6Xp?<V{T0_UC5}uov>vhdbXJQNM-^DB~!^O1GAL5UB~c(0%v#ugm&SUH7Az
z-n5a*SCnEA8EA<GrFag9G~rIUb$43h8`*=m;4zIxzCMon9b`+9#UfT3nXu#($?HO-
zH=`cPlO$?m)2MxD*rsTuNOR}}wTR$IX($iH%YlE8AlMz&LVKyh<rci(CVtEc%lxP`
zVHMN=K9E(sRjWF+0?ZDtMSGq<%lVt(AMdJ+?RYU{&S=fOm#sQ+`s1^<HcP|BNm+dl
zm~%ZK{oD7Sc;T>Qw#y+pFI<r@9W!X=YkOR_-%V4geYi#!40Jc_zu_y)phFGSlf|{2
zO2~X|Ct+y=K_wE^R&tPX>+y%aebxNcujwsgdm1}rsK+z`HWTwxz}Urq2^-|QOqV{F
zy{pl*oFe1(WJ7Fjj-&vcfhQ90lKKJ0GON?!a+J8)>|%G7!+G*wDjt9lOS=`EVheOY
z3ARBNvY{B?Rj6|Ux<Lwoe6cbEH1j*FBm7<UJQiy`_BjTo21!qdN9*aQG^dGJ5G~hG
q0U{v>n+`RyW}dBPA<(2#=l!zimlgef8pD%>WMV<IxW{hwfBOR$(KA{A

literal 0
HcmV?d00001

diff --git a/testlink5.lnk b/testlink5.lnk
new file mode 100644
index 0000000000000000000000000000000000000000..d4866223362b1c9741c2b2a023d7482dc81cf4dd
GIT binary patch
literal 2037
zcmds1T}V_x6h3Z4nEh@Uh*(}FA)~fc6Um=aub9=YYwZ?`+y`aRv=%SzE-5`!_9H??
zl$gF)`;fh)B}n+NBtj1r5oHg7Q4*<yR&E+ZP+;G=dsi$&qo4=R<va7unKNhRoSC^6
zB8rGMKoj+8nhLKNB1r-t_|np{JML-3uWiT;^TI@L&!de4LeWA-?$-eObvyS%Vjz>w
z6vbkb^DIR+yF=N@LJB?$*+`~5v_)Wca=?~v4u3>X#@~J$l_p2VCMGpDJU;gM!^p=;
zldsN4MlFP!RB}-{xv2`wOTM2%1hD@lME-tkBgI}~H%}z>1b4-*YM(HaUAXqX!wP#8
zZ!sQN#}GS2s@N;G3yJbYy2uoHB7cT$8+fLWMK;EAFk-@jbpT5;AeeBlrN9PZMxZQ&
zN<?Z-Ad!b@EwGYFE~~WL+6l068TXllh4~8Z<g(H|neGEPA&c|PP-9Q&o`7_+wBS+4
z>vY%`vjgv8HNa(_yGHCybP?cyHxIiu<}^Py-hDxRwt<*uM`2&j4jgbRFn>VhpQ^}f
zu{^-hp&tHdigzxL-TWz`w?uC2@*etrHJIIv@lFcdY0A1DsyOb@?+-W$_UQv=GP@Qq
zLbd>$f98#QgWVZt<je00T3;qN-!j6!h#l6j(k1Ndl}Dm_<xbnV;p$fnX{|Nv%m=Tj
zsVUuCYmU+Vh<-(JtFCf)mFo5BufSUE@~B?5(kB&H_-a+pkv}Ogk(H`(>pj|ySE&*g
zP@-b0KwAr&hY$v0pi`8pvFw7!4H9LT&GLy9lf&sC%!x$lg1tP#*aW}h2iRG=KH991
z`uislivZCm9!}CfD|Kbg$30{Hg+u?>(^)tbTbP-YAuT)$zf(_d`lk?1_iI8+Oxj>!
gdwt{RS#z-dVOyKM>GY8CQWupR?8C#IZya%c00fmUg#Z8m

literal 0
HcmV?d00001

diff --git a/testlink6.lnk b/testlink6.lnk
new file mode 100644
index 0000000000000000000000000000000000000000..346797d658169a1fec2e5600cf9e800c07def958
GIT binary patch
literal 2098
zcmds1Ur1A76hB*+F#Ds@0<k|{#f3)6ETPm!>Q<>uH`<UPm(qsjO1ie)QeO;>LNJIF
zLm$#Xe_qrwBz%x;&{I*AJp}z1Ou{0T&jt27cXw%O7D6wc@jK^y=bWAMedqhmwGold
zpa&-ks+<nA>7z)5J~?@$N!a{MH&=$k@V|(n$u`IoAe=31<aJS4rDKp=0rnd-8?PkE
zX1C<o3hYjoH2OIB^bNUua;?`Ri<#2C2&F6a3c1KfGS!loJdh$~l0^BYxxaK$s2(8N
z2){%%i1knfWHmHDJQ5`d*@8(h3x@=0fvneYv{gibyU3B@bT~~@#-Y%`gxH%?`tnQN
zU;1{y9?d661l9p_w~%j|*+d%r0-y%3hD<@_EFnh_DH*lhKr*{&0}#VXF0+`TW^O(k
z*YKK^2(iv(e~aXGCBPfDxyQy^hRTYD#dBrWM}6-KVPDAsyx~p2()z*~h?)Q{a_fXA
z)!gwS?%qoZ+F!*#=i*y9fQxJcmR4l(E9Lx5I=mwEC19XL_u7Q*-;zcgmX-nk@gG-1
zdk5j~r{O#8IX&UdQywe}U8E8AUl+;B{I>!lh;N(|WZ$^|esK4B%au>o&ev%jy>YOw
z;DGhaG>QcKEfdirmVWz%kKVTzcXgiPV1589o1WI9RdJRc>P980M)p+HRLg!v8vQ)>
z^*a@NJwDklS1DpirBWyRPX0?%iHg+;;aTwmRe13tmEdtGREM>XkOrj3+sjZjv$d-s
zmRyJJjzUW#Ot}<=#)U>wtwtC7_%i+KenV>f44XfZMDdvPom$@329fq;nz9-*(p_#B
zQGu4(Y;A2|aOQ~~j2r2g&7#FdDgki@v>ks5%xjwTr?1DX<jYL`pM$o{TD7c?Q;ySN
xECO!rl)pYF+B)TovG>UX$NsL;+a;!>r$12lHD`PCc--Xya+C%s>TiBd^AidxC?)^^

literal 0
HcmV?d00001

-- 
GitLab