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