├── .gitignore ├── LICENSE ├── README.md ├── bytes.go ├── bytes_test.go ├── extradata.go ├── file.go ├── go.mod ├── go.sum ├── header.go ├── headerutils.go ├── headerutils_test.go ├── idlist.go ├── img ├── example01.png ├── example02.png └── example03.png ├── linkinfo.go ├── linkinfo_network.go ├── linkinfo_volumeid.go ├── stringdata.go ├── test ├── Visual Studio Code.lnk ├── Windows Store.lnk ├── funcs.go ├── m2.go ├── main.go ├── nem.test ├── parseStartMenu.go ├── remote.directory.xp.test ├── remote.file.xp.test ├── test-orig.lnk ├── test.lnk ├── test.lnk.bak └── vbox-svr-win10.lnk └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | debug.test 2 | /vendor -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lnk - lnk Parser for Go 2 | lnk is a package for parsing Windows Shell Link (.lnk) files. 3 | 4 | It's based on version 5.0 of the [MS-SHLLINK] document: 5 | 6 | * Reference: https://msdn.microsoft.com/en-us/library/dd871305.aspx 7 | * Version 5.0: https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-SHLLINK/[MS-SHLLINK].pdf 8 | 9 | If the lnk file does not adhere to this specification (either corrupted or from an earlier version), it might not be parsed. 10 | 11 | ## Shell Link Structure 12 | Each file has at least one header (`SHELL_LINK_HEADER`) and one or more optional sections. 13 | 14 | ``` 15 | SHELL_LINK = SHELL_LINK_HEADER [LINKTARGET_IDLIST] [LINKINFO] 16 | [STRING_DATA] *EXTRA_DATA 17 | ``` 18 | 19 | The existence of these sections are defined by the `LinkFlags` uint32 in the header (mapped to `HEADER.LinkFlags`). To see all flags, look at `linkFlags` in [header.go](header.go). 20 | 21 | Note about size fields: "Unless otherwise specified, the value contained by size fields includes the size of size field itself." 22 | 23 | Currently lnk parses every section except `EXTRA_DATA`. Different data blocks are identified and stored but it does not parse any of them other than identifying the type (via their signature) and storing the content. Data blocks are defined in section 2.5 of the specification. 24 | 25 | ## Setup 26 | Package has only one dependency: https://github.com/olekukonko/tablewriter. It's used to create tables in section stringers. 27 | 28 | ## Usage 29 | Pass a filename to `lnk.File` or an `io.Reader` with its contents to `lnk.Read`. Both return `LnkFile`: 30 | 31 | ``` go 32 | type LnkFile struct { 33 | Header ShellLinkHeaderSection // File header. 34 | IDList LinkTargetIDListSection // LinkTargetIDList. 35 | LinkInfo LinkInfoSection // LinkInfo. 36 | StringData StringDataSection // StringData. 37 | DataBlocks ExtraDataSection // ExtraData blocks. 38 | } 39 | ``` 40 | 41 | Each section is a struct that is populated. See their fields in their respective source files. 42 | 43 | ``` go 44 | package main 45 | 46 | import ( 47 | "fmt" 48 | 49 | "github.com/parsiya/golnk" 50 | ) 51 | 52 | func main() { 53 | 54 | Lnk, err := lnk.File("test.lnk") 55 | if err != nil { 56 | panic(err) 57 | } 58 | 59 | // Print header. 60 | fmt.Println(Lnk.Header) 61 | 62 | // Path to the target file is usually in LinkInfo.LocalBasePath. 63 | fmt.Println("BasePath", Lnk.LinkInfo.LocalBasePath) 64 | 65 | // fmt.Println(Lnk.LinkInfo) 66 | 67 | // fmt.Println(Lnk.StringData) 68 | 69 | // fmt.Println(Lnk.DataBlocks) 70 | } 71 | ``` 72 | 73 | ![header printed](img/example01.png) 74 | 75 | Each section has a [Stringer](https://golang.org/pkg/fmt/#Stringer) that prints the fields in a [table](https://github.com/olekukonko/tablewriter). 76 | 77 | ![link info printed](img/example02.png) 78 | 79 | Extra Data Blocks are not parsed but can be dumped or accessed manually. 80 | 81 | ![extra data block dump](img/example03.png) 82 | 83 | **Parse the Windows start menu and extract the base path for all lnk files.** 84 | 85 | See [test/parseStartMenu.go](test/parseStartMenu.go): 86 | 87 | ``` go 88 | package main 89 | 90 | import ( 91 | "fmt" 92 | "os" 93 | "path/filepath" 94 | 95 | "github.com/parsiya/golnk" 96 | ) 97 | 98 | // Sample program to parse all lnk files in the "All Users" start menu at 99 | // C:\ProgramData\Microsoft\Windows\Start Menu\Programs. 100 | 101 | func main() { 102 | startMenu := "C:/ProgramData/Microsoft/Windows/Start Menu/Programs" 103 | basePaths := []string{} 104 | err := filepath.Walk(startMenu, func(path string, info os.FileInfo, walkErr error) error { 105 | // Only look for lnk files. 106 | if filepath.Ext(info.Name()) == ".lnk" { 107 | f, lnkErr := lnk.File(path) 108 | // Print errors and move on to the next file. 109 | if lnkErr != nil { 110 | fmt.Println(lnkErr) 111 | return nil 112 | } 113 | var targetPath = "" 114 | if f.LinkInfo.LocalBasePath != "" { 115 | targetPath = f.LinkInfo.LocalBasePath 116 | } 117 | if f.LinkInfo.LocalBasePathUnicode != "" { 118 | targetPath = f.LinkInfo.LocalBasePathUnicode 119 | } 120 | if targetPath != "" { 121 | fmt.Println("Found", targetPath) 122 | basePaths = append(basePaths, targetPath) 123 | } 124 | } 125 | return nil 126 | }) 127 | if err != nil { 128 | panic(err) 129 | } 130 | 131 | // Print everything. 132 | fmt.Println("------------------------") 133 | for _, p := range basePaths { 134 | fmt.Println(p) 135 | } 136 | } 137 | ``` 138 | 139 | ## TODO 140 | 1. Use `dep`? 141 | 2. Identify ExtraDataBlocks. 142 | 3. Clean up code. 143 | 4. Write more unit tests. 144 | 5. Test it on more lnk files. 145 | 6. ~~Add a `Data` field to each section and store raw bytes there. Then add a `Dump` method to each section and use `hex.Dump` to dump the raw bytes.~~ 146 | -------------------------------------------------------------------------------- /bytes.go: -------------------------------------------------------------------------------- 1 | package lnk 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "io" 8 | "unicode/utf8" 9 | ) 10 | 11 | // byteMaskuint16 returns one of the two bytes from a uint16. 12 | func byteMaskuint16(b uint16, n int) uint16 { 13 | // Maybe we should not panic, hmm. 14 | if n < 0 || n > 2 { 15 | panic(fmt.Sprintf("invalid byte mask, got %d", n)) 16 | } 17 | mask := uint16(0x000000FF) << uint16(n*8) 18 | return (b & mask) >> uint16(n*8) 19 | } 20 | 21 | // bitMaskuint32 returns one of the 32-bits from a uint32. 22 | // Returns true for 1 and false for 0. 23 | func bitMaskuint32(b uint32, n int) bool { 24 | if n < 0 || n > 31 { 25 | panic(fmt.Sprintf("invalid bit number, got %d", n)) 26 | } 27 | return ((b >> uint(n)) & 1) == 1 28 | } 29 | 30 | // ReadBytes reads n bytes from the slice starting from offset and 31 | // returns a []byte and the number of bytes read. If offset is out of bounds 32 | // it returns an empty []byte and 0 bytes read. 33 | // TODO: Write tests for this. 34 | func ReadBytes(b []byte, offset, num int) (out []byte, n int) { 35 | if offset >= len(b) { 36 | return out, 0 37 | } 38 | if offset+num >= len(b) { 39 | return b[offset:], len(b[offset:]) 40 | } 41 | return b[offset : offset+num], num 42 | } 43 | 44 | /* 45 | readSection reads a size from the start of the io.Reader. The size length is 46 | decided by the parameter sSize. 47 | sSize == 2 - read uint16 48 | sSize == 4 - read uint32 49 | sSize == 8 - read uint64 - Not needed for now. 50 | Then read (size-sSize) bytes, populate the start with the original bytes 51 | and add the rest. Finally return the []byte and a new io.Reader to it. 52 | The size bytes are added to the start of the []byte to keep the section 53 | []byte intact for later offset use. 54 | */ 55 | func readSection(r io.Reader, sSize int, maxSize uint64) (data []byte, nr io.Reader, size int, err error) { 56 | // We are not going to lose data by copying a smaller var into a larger one. 57 | var sectionSize uint64 58 | switch sSize { 59 | case 2: 60 | // Read uint16. 61 | var size16 uint16 62 | err = binary.Read(r, binary.LittleEndian, &size16) 63 | if err != nil { 64 | return data, nr, size, fmt.Errorf("golnk.readSection: read size %d bytes - %s", sSize, err.Error()) 65 | } 66 | sectionSize = uint64(size16) 67 | // Add bytes to the start of data []byte. 68 | data = uint16Byte(size16) 69 | case 4: 70 | // Read uint32. 71 | var size32 uint32 72 | err = binary.Read(r, binary.LittleEndian, &size32) 73 | if err != nil { 74 | return data, nr, size, fmt.Errorf("golnk.readSection: read size %d bytes - %s", sSize, err.Error()) 75 | } 76 | sectionSize = uint64(size32) 77 | // Add bytes to the start of data []byte. 78 | data = uint32Byte(size32) 79 | case 8: 80 | // Read uint64 or sectionSize. 81 | err = binary.Read(r, binary.LittleEndian, §ionSize) 82 | if err != nil { 83 | return data, nr, size, fmt.Errorf("golnk.readSection: read size %d bytes - %s", sSize, err.Error()) 84 | } 85 | // Add bytes to the start of data []byte. 86 | data = uint64Byte(sectionSize) 87 | default: 88 | return data, nr, size, fmt.Errorf("golnk.readSection: invalid sSize - got %v", sSize) 89 | } 90 | 91 | // Create a []byte of sectionSize-4 and read that many bytes from io.Reader. 92 | computedSize := sectionSize - uint64(sSize) 93 | if computedSize > maxSize { 94 | return data, nr, size, fmt.Errorf("golnk.readSection: invalid computed size got %d; expected a size < %d", computedSize, maxSize) 95 | } 96 | 97 | tempData := make([]byte, computedSize) 98 | err = binary.Read(r, binary.LittleEndian, &tempData) 99 | if err != nil { 100 | return data, nr, size, fmt.Errorf("golnk.readSection: read section %d bytes - %s", sectionSize-uint64(sSize), err.Error()) 101 | } 102 | 103 | // If this is successful, append it to data []byte. 104 | data = append(data, tempData...) 105 | 106 | // Create a reader from the unread bytes. 107 | nr = bytes.NewReader(tempData) 108 | 109 | return data, nr, int(sectionSize), nil 110 | } 111 | 112 | // readString returns a string of all bytes from the []byte until the first 0x00. 113 | func readString(data []byte) string { 114 | // Find the index of first 0x00. 115 | i := bytes.IndexByte(data, byte(0x00)) 116 | if i == -1 { 117 | // If 0x00 is not found, return all the slice. 118 | i = len(data) 119 | } 120 | return string(data[:i]) 121 | } 122 | 123 | // readUnicodeString returns a string of all bytes from the []byte until the 124 | // first 0x0000. 125 | func readUnicodeString(data []byte) string { 126 | 127 | // Read two bytes at a time and convert to rune, stop if both are 0x0000 or 128 | // we have reached the end of the input. 129 | var runes []rune 130 | for bitIndex := 0; bitIndex < len(data)/2; bitIndex++ { 131 | if data[bitIndex*2] == 0x00 && data[(bitIndex*2)+1] == 0x00 { 132 | return string(runes) 133 | } 134 | r, _ := utf8.DecodeRune(data[bitIndex*2:]) 135 | runes = append(runes, r) 136 | } 137 | return string(runes) 138 | } 139 | 140 | // readStringData reads a uint16 as size and then reads that many bytes 141 | // (*2 for unicode) into a string. The string is not null-terminated. 142 | // TODO: Write tests. 143 | func readStringData(r io.Reader, isUnicode bool) (str string, err error) { 144 | // Recover in case we attempt to read more bytes than there is in the reader. 145 | defer func() { 146 | if r := recover(); r != nil { 147 | // If panic occurs, return this error message 148 | err = fmt.Errorf("golnk.readStringData: not enough bytes in reader") 149 | } 150 | }() 151 | 152 | var size uint16 153 | err = binary.Read(r, binary.LittleEndian, &size) 154 | if err != nil { 155 | return str, fmt.Errorf("golnk.readStringData: read size - %s", err.Error()) 156 | } 157 | if isUnicode { 158 | size = size * 2 159 | } 160 | b := make([]byte, size) 161 | err = binary.Read(r, binary.LittleEndian, &b) 162 | if err != nil { 163 | return str, fmt.Errorf("golnk.readStringData: read bytes - %s", err.Error()) 164 | } 165 | // If unicode, read every 2 byte and get a rune. 166 | if isUnicode { 167 | var runes []rune 168 | for bitIndex := 0; bitIndex < int(size)/2; bitIndex++ { 169 | r, _ := utf8.DecodeRune(b[bitIndex*2:]) 170 | runes = append(runes, r) 171 | } 172 | return string(runes), nil 173 | } 174 | return string(b), nil 175 | } 176 | 177 | // uint16Little reads a uint16 from []byte and returns the result in Little-Endian. 178 | func uint16Little(b []byte) uint16 { 179 | if len(b) < 2 { 180 | panic(fmt.Sprintf("input smaller than two bytes - got %d", len(b))) 181 | } 182 | return binary.LittleEndian.Uint16(b) 183 | } 184 | 185 | // uint32Little reads a uint32 from []byte and returns the result in Little-Endian. 186 | func uint32Little(b []byte) uint32 { 187 | if len(b) < 4 { 188 | panic(fmt.Sprintf("input smaller than four bytes - got %d", len(b))) 189 | } 190 | return binary.LittleEndian.Uint32(b) 191 | } 192 | 193 | // uint64Little reads a uint64 from []byte and returns the result in Little-Endian. 194 | func uint64Little(b []byte) uint64 { 195 | if len(b) < 8 { 196 | panic(fmt.Sprintf("input smaller than eight bytes - got %d", len(b))) 197 | } 198 | return binary.LittleEndian.Uint64(b) 199 | } 200 | 201 | // uint16Str converts a uint16 to string using fmt.Sprint. 202 | func uint16Str(u uint16) string { 203 | return fmt.Sprint(u) 204 | } 205 | 206 | // int16Str converts an int16 to string using fmt.Sprint. 207 | func int16Str(u int16) string { 208 | return fmt.Sprint(u) 209 | } 210 | 211 | // uint32Str converts a uint32 to string using fmt.Sprint. 212 | func uint32Str(u uint32) string { 213 | return fmt.Sprint(u) 214 | } 215 | 216 | // uint32StrHex converts a uint32 to a hex encoded string using fmt.Sprintf. 217 | func uint32StrHex(u uint32) string { 218 | str := fmt.Sprintf("%x", u) 219 | // Add a 0 to the start of odd-length string. This converts "0x1AB" to "0x01AB" 220 | if (len(str) % 2) != 0 { 221 | str = "0" + str 222 | } 223 | return "0x" + str 224 | } 225 | 226 | // uint32TableStr creates a string that has both decimal and hex values 227 | // of uint32. 228 | func uint32TableStr(u uint32) string { 229 | return fmt.Sprintf("%s - %s", uint32Str(u), uint32StrHex(u)) 230 | } 231 | 232 | // int32Str converts an int32 to string using fmt.Sprint. 233 | func int32Str(u int32) string { 234 | return fmt.Sprint(u) 235 | } 236 | 237 | // uint16Byte converts a uint16 to a []byte. 238 | func uint16Byte(u uint16) []byte { 239 | var buf bytes.Buffer 240 | err := binary.Write(&buf, binary.LittleEndian, u) 241 | if err != nil { 242 | panic(err) 243 | } 244 | return buf.Bytes() 245 | } 246 | 247 | // uint32Byte converts a uint32 to a []byte. 248 | func uint32Byte(u uint32) []byte { 249 | var buf bytes.Buffer 250 | err := binary.Write(&buf, binary.LittleEndian, u) 251 | if err != nil { 252 | panic(err) 253 | } 254 | return buf.Bytes() 255 | } 256 | 257 | // uint64Byte converts a uint64 to a []byte. 258 | func uint64Byte(u uint64) []byte { 259 | var buf bytes.Buffer 260 | err := binary.Write(&buf, binary.LittleEndian, u) 261 | if err != nil { 262 | panic(err) 263 | } 264 | return buf.Bytes() 265 | } 266 | -------------------------------------------------------------------------------- /bytes_test.go: -------------------------------------------------------------------------------- 1 | package lnk 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | var ( 8 | b0 = []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07} 9 | b1 = []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A} 10 | b2 = []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D} 11 | b3 = []byte{0xFF, 0xEE, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D} 12 | b4 = []byte{0xCC, 0xDD, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D} 13 | b5 = []byte{0xBB, 0xAA, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D} 14 | ) 15 | 16 | func Test_uint16Little(t *testing.T) { 17 | type args struct { 18 | b []byte 19 | } 20 | tests := []struct { 21 | name string 22 | args args 23 | want uint16 24 | }{ 25 | {"b0", args{b0}, 0x0100}, 26 | {"b1", args{b1}, 0x0100}, 27 | {"b2", args{b2}, 0x0100}, 28 | {"b3", args{b3}, 0xEEFF}, 29 | {"b4", args{b4}, 0xDDCC}, 30 | {"b5", args{b5}, 0xAABB}, 31 | } 32 | for _, tt := range tests { 33 | t.Run(tt.name, func(t *testing.T) { 34 | if got := uint16Little(tt.args.b); got != tt.want { 35 | t.Errorf("uint16Little() = %v, want %v", got, tt.want) 36 | } 37 | }) 38 | } 39 | } 40 | 41 | func Test_uint32Little(t *testing.T) { 42 | type args struct { 43 | b []byte 44 | } 45 | tests := []struct { 46 | name string 47 | args args 48 | want uint32 49 | }{ 50 | {"b0", args{b0}, 0x03020100}, 51 | {"b1", args{b1}, 0x03020100}, 52 | {"b2", args{b2}, 0x03020100}, 53 | {"b3", args{b3}, 0x0302EEFF}, 54 | {"b4", args{b4}, 0x0302DDCC}, 55 | {"b5", args{b5}, 0x0302AABB}, 56 | } 57 | for _, tt := range tests { 58 | t.Run(tt.name, func(t *testing.T) { 59 | if got := uint32Little(tt.args.b); got != tt.want { 60 | t.Errorf("uint32Little() = %v, want %v", got, tt.want) 61 | } 62 | }) 63 | } 64 | } 65 | 66 | func Test_uint64Little(t *testing.T) { 67 | type args struct { 68 | b []byte 69 | } 70 | tests := []struct { 71 | name string 72 | args args 73 | want uint64 74 | }{ 75 | {"b0", args{b0}, 0x0706050403020100}, 76 | {"b1", args{b1}, 0x0706050403020100}, 77 | {"b2", args{b2}, 0x0706050403020100}, 78 | {"b3", args{b3}, 0x070605040302EEFF}, 79 | {"b4", args{b4}, 0x070605040302DDCC}, 80 | {"b5", args{b5}, 0x070605040302AABB}, 81 | } 82 | for _, tt := range tests { 83 | t.Run(tt.name, func(t *testing.T) { 84 | if got := uint64Little(tt.args.b); got != tt.want { 85 | t.Errorf("uint64Little() = %v, want %v", got, tt.want) 86 | } 87 | }) 88 | } 89 | } 90 | 91 | func Test_byteMaskuint16(t *testing.T) { 92 | type args struct { 93 | b uint16 94 | n int 95 | } 96 | tests := []struct { 97 | name string 98 | args args 99 | want uint16 100 | }{ 101 | {"highbyte", args{uint16(0xAABB), 1}, 0xAA}, 102 | {"lowbyte", args{uint16(0xAABB), 0}, 0xBB}, 103 | {"highbyte-zero", args{uint16(0x00BB), 1}, 0x00}, 104 | {"lowbyte-zero", args{uint16(0xAA00), 0}, 0x00}, 105 | {"byte-0", args{b: 0xBBAA, n: 0}, 0x00AA}, 106 | {"byte-1", args{b: 0xBBAA, n: 1}, 0x00BB}, 107 | {"invalid", args{b: 0x0201, n: 0}, 0x0001}, 108 | {"byte-1", args{b: 0x0201, n: 1}, 0x0002}, 109 | } 110 | for _, tt := range tests { 111 | t.Run(tt.name, func(t *testing.T) { 112 | if got := byteMaskuint16(tt.args.b, tt.args.n); got != tt.want { 113 | t.Errorf("byteMaskuint16() = %v, want %v", got, tt.want) 114 | } 115 | }) 116 | } 117 | } 118 | 119 | func Test_bitMaskuint32(t *testing.T) { 120 | // 255 is 1111 1111 121 | u255 := uint32(255) 122 | // 0 is 0000 0000 123 | u0 := uint32(0) 124 | type args struct { 125 | b uint32 126 | n int 127 | } 128 | tests := []struct { 129 | name string 130 | args args 131 | want bool 132 | }{ 133 | {"bit0-255", args{u255, 0}, true}, 134 | {"bit1-255", args{u255, 0}, true}, 135 | {"bit2-255", args{u255, 0}, true}, 136 | {"bit3-255", args{u255, 0}, true}, 137 | {"bit4-255", args{u255, 0}, true}, 138 | {"bit5-255", args{u255, 0}, true}, 139 | {"bit6-255", args{u255, 0}, true}, 140 | {"bit7-255", args{u255, 0}, true}, 141 | {"bit0-0", args{u0, 0}, false}, 142 | {"bit1-0", args{u0, 0}, false}, 143 | {"bit2-0", args{u0, 0}, false}, 144 | {"bit3-0", args{u0, 0}, false}, 145 | {"bit4-0", args{u0, 0}, false}, 146 | {"bit5-0", args{u0, 0}, false}, 147 | {"bit6-0", args{u0, 0}, false}, 148 | {"bit7-0", args{u0, 0}, false}, 149 | } 150 | for _, tt := range tests { 151 | t.Run(tt.name, func(t *testing.T) { 152 | if got := bitMaskuint32(tt.args.b, tt.args.n); got != tt.want { 153 | t.Errorf("bitMaskuint32() = %v, want %v", got, tt.want) 154 | } 155 | }) 156 | } 157 | } 158 | 159 | func Test_readString(t *testing.T) { 160 | type args struct { 161 | data []byte 162 | } 163 | tests := []struct { 164 | name string 165 | args args 166 | want string 167 | }{ 168 | {"normal-0123", args{[]byte{0x30, 0x31, 0x32, 0x33, 0x00, 0x34, 0x35, 0x36}}, "0123"}, 169 | {"no-0x00", args{[]byte{0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36}}, "0123456"}, 170 | {"start-0x00", args{[]byte{0x00, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36}}, ""}, 171 | {"multiple-sequential-0x00", args{[]byte{0x30, 0x31, 0x32, 0x33, 0x00, 0x00, 0x34, 0x35, 0x36}}, "0123"}, 172 | } 173 | for _, tt := range tests { 174 | t.Run(tt.name, func(t *testing.T) { 175 | if got := readString(tt.args.data); got != tt.want { 176 | t.Errorf("readString() = %v, want %v", got, tt.want) 177 | } 178 | }) 179 | } 180 | } 181 | 182 | func Test_readUnicodeString(t *testing.T) { 183 | type args struct { 184 | data []byte 185 | } 186 | tests := []struct { 187 | name string 188 | args args 189 | want string 190 | }{ 191 | {"normal-0123", args{[]byte{0x30, 0x00, 0x31, 0x00, 0x32, 0x00, 0x33, 0x00, 0x00, 0x00}}, "0123"}, 192 | {"normal-0123-2", args{[]byte{0x30, 0x00, 0x31, 0x00, 0x32, 0x00, 0x33, 0x00, 0x00, 0x00, 0x34, 0x00, 0x35, 0x00, 0x36, 0x00}}, "0123"}, 193 | {"no-0x00", args{[]byte{0x30, 0x00, 0x31, 0x00, 0x32, 0x00, 0x33, 0x00, 0x34, 0x00, 0x35, 0x00, 0x36, 0x00}}, "0123456"}, 194 | {"start-0x00", args{[]byte{0x00, 0x00, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36}}, ""}, 195 | } 196 | for _, tt := range tests { 197 | t.Run(tt.name, func(t *testing.T) { 198 | if got := readUnicodeString(tt.args.data); got != tt.want { 199 | t.Errorf("readUnicodeString() = %v, want %v", got, tt.want) 200 | } 201 | }) 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /extradata.go: -------------------------------------------------------------------------------- 1 | package lnk 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/hex" 6 | "fmt" 7 | "io" 8 | "strings" 9 | ) 10 | 11 | // ExtraDataSection represents section 2.5 of the specification. 12 | type ExtraDataSection struct { 13 | Blocks []ExtraDataBlock 14 | // Terminal block at the end of the ExtraData section. 15 | // Value must be smaller than 0x04. 16 | TerminalBlock uint32 17 | } 18 | 19 | /* 20 | ExtraDataBlock represents one of the optional data blocks at the end of the 21 | lnk file. 22 | Each data block starts with a uint32 size and a uint32 signature. 23 | Detection is as follows: 24 | 1. Read the uint32 size. If size < 0x04, it's the terminal block. 25 | 2. Read the datablock (size-4) more bytes from the io.Reader. 26 | 3. Read the uint32 signature. It will designate the datablock. 27 | 4. Parse the data based on the signature. 28 | */ 29 | type ExtraDataBlock struct { 30 | Size uint32 31 | Signature uint32 32 | Type string 33 | Data []byte 34 | } 35 | 36 | // DataBlock reads and populates an ExtraData. 37 | func DataBlock(r io.Reader) (extra ExtraDataSection, err error) { 38 | 39 | var db ExtraDataBlock 40 | for { 41 | // Read size. 42 | var size uint32 43 | err = binary.Read(r, binary.LittleEndian, &size) 44 | if err != nil { 45 | return extra, fmt.Errorf("golnk.readDataBlock: read size - %s", err.Error()) 46 | } 47 | // fmt.Println("Size", size) 48 | // Have we reached the TerminalBlock? 49 | if size < 0x04 { 50 | extra.TerminalBlock = size 51 | break 52 | } 53 | db.Size = size 54 | 55 | // Read block's signature. 56 | err = binary.Read(r, binary.LittleEndian, &db.Signature) 57 | if err != nil { 58 | return extra, fmt.Errorf("golnk.readDataBlock: read signature - %s", err.Error()) 59 | } 60 | // fmt.Println("Signature", hex.EncodeToString(uint32Byte(db.Signature))) 61 | db.Type = blockSignature(db.Signature) 62 | // fmt.Println("Type:", db.Type) 63 | 64 | // Read the rest of the data. Size-8. 65 | data := make([]byte, db.Size-8) 66 | err = binary.Read(r, binary.LittleEndian, &data) 67 | if err != nil { 68 | return extra, fmt.Errorf("golnk.readDataBlock: read data - %s", err.Error()) 69 | } 70 | db.Data = data 71 | // fmt.Println(hex.Dump(data)) 72 | extra.Blocks = append(extra.Blocks, db) 73 | } 74 | return extra, nil 75 | } 76 | 77 | // blockSignature returns the block type based on signature. 78 | func blockSignature(sig uint32) string { 79 | signatureMap := map[uint32]string{ 80 | 0xA0000002: "ConsoleDataBlock", 81 | 0xA0000004: "ConsoleFEDataBlock", 82 | 0xA0000006: "DarwinDataBlock", 83 | 0xA0000001: "EnvironmentVariableDataBlock", 84 | 0xA0000007: "IconEnvironmentDataBlock", 85 | 0xA0000009: "PropertyStoreDataBlock", 86 | 0xA0000008: "ShimDataBlock", 87 | 0xA0000005: "SpecialFolderDataBlock", 88 | 0xA0000003: "TrackerDataBlock", 89 | 0xA000000C: "VistaAndAboveIDListDataBlock", 90 | 0xA000000B: "KnownFolderDataBlock", 91 | } 92 | if val, exists := signatureMap[sig]; exists { 93 | return val 94 | } 95 | return "Signature Not Found - " + hex.EncodeToString(uint32Byte(sig)) 96 | } 97 | 98 | // String prints the ExtraData blocks' Type, Size, and a hexdump of their content. 99 | func (e ExtraDataSection) String() string { 100 | 101 | var sb strings.Builder 102 | for _, b := range e.Blocks { 103 | sb.WriteString(fmt.Sprintf("Size: %s\n", uint32TableStr(b.Size))) 104 | sb.WriteString(fmt.Sprintf("Signature: %s\n", uint32StrHex(b.Signature))) 105 | sb.WriteString(fmt.Sprintf("Type: %s\n", b.Type)) 106 | sb.WriteString("Dump\n") 107 | sb.WriteString(b.Dump()) 108 | sb.WriteString("-------------------------\n") 109 | } 110 | return sb.String() 111 | } 112 | 113 | // Dump returns the hex.Dump of ExtraDataBlock. 114 | func (db ExtraDataBlock) Dump() string { 115 | return hex.Dump(db.Data) 116 | } 117 | -------------------------------------------------------------------------------- /file.go: -------------------------------------------------------------------------------- 1 | package lnk 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | ) 8 | 9 | // LnkFile represents one lnk file. 10 | type LnkFile struct { 11 | Header ShellLinkHeaderSection // File header. 12 | IDList LinkTargetIDListSection // LinkTargetIDList. 13 | LinkInfo LinkInfoSection // LinkInfo. 14 | StringData StringDataSection // StringData. 15 | DataBlocks ExtraDataSection // ExtraData blocks. 16 | } 17 | 18 | // Read parses an io.Reader pointing to the contents of an lnk file. 19 | func Read(r io.Reader, maxSize uint64) (f LnkFile, err error) { 20 | 21 | f.Header, err = Header(r, maxSize) 22 | if err != nil { 23 | return f, fmt.Errorf("golnk.Read: parse Header - %s", err.Error()) 24 | } 25 | 26 | // If HasLinkTargetIDList is set, header is immediately followed by a LinkTargetIDList. 27 | if f.Header.LinkFlags["HasLinkTargetIDList"] { 28 | f.IDList, err = LinkTarget(r) 29 | if err != nil { 30 | return f, fmt.Errorf("golnk.Read: parse LinkTarget - %s", err.Error()) 31 | } 32 | } 33 | 34 | // If HasLinkInfo is set, read LinkInfo section. 35 | if f.Header.LinkFlags["HasLinkInfo"] { 36 | f.LinkInfo, err = LinkInfo(r, maxSize) 37 | if err != nil { 38 | return f, fmt.Errorf("golnk.Read: parse LinkInfo - %s", err.Error()) 39 | } 40 | } 41 | 42 | // Read StringData section. 43 | f.StringData, err = StringData(r, f.Header.LinkFlags) 44 | if err != nil { 45 | return f, fmt.Errorf("golnk.Read: parse StringData - %s", err.Error()) 46 | } 47 | 48 | f.DataBlocks, err = DataBlock(r) 49 | if err != nil { 50 | return f, fmt.Errorf("golnk.Read: parse ExtraDataBlock - %s", err.Error()) 51 | } 52 | 53 | return f, err 54 | } 55 | 56 | // File parses an lnk File. 57 | func File(filename string) (f LnkFile, err error) { 58 | fi, err := os.Open(filename) 59 | if err != nil { 60 | return f, fmt.Errorf("golnk.File: open file - %s", err.Error()) 61 | } 62 | defer fi.Close() 63 | 64 | // To try and detect malformed lnk files, we'll make sure no section is bigger than the actual file size as that 65 | // shouldn't ever happen 66 | maxSize := uint64(1 << 22) 67 | if s, err := fi.Stat(); err == nil { 68 | if maxSize > 0 { 69 | maxSize = uint64(s.Size()) 70 | } 71 | } 72 | 73 | return Read(fi, maxSize) 74 | } 75 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/parsiya/golnk 2 | 3 | go 1.13 4 | 5 | require github.com/olekukonko/tablewriter v0.0.5 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= 2 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 3 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 4 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 5 | -------------------------------------------------------------------------------- /header.go: -------------------------------------------------------------------------------- 1 | package lnk 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/hex" 6 | "fmt" 7 | "io" 8 | "strings" 9 | "time" 10 | 11 | "github.com/olekukonko/tablewriter" 12 | ) 13 | 14 | const ( 15 | headerSize = 0x4C 16 | // This is how it appears in the file when we read an array. 17 | // Endian-ness does not make a difference. 18 | classID = "0114020000000000c000000000000046" 19 | ) 20 | 21 | type FlagMap map[string]bool 22 | 23 | // From the docs: 24 | // "Multi-byte data values in the Shell Link Binary File Format are stored in little-endian format." 25 | 26 | // ShellLinkHeaderSection represents the lnk header. 27 | type ShellLinkHeaderSection struct { 28 | Magic uint32 // Header size: should be 0x4c. 29 | LinkCLSID [16]byte // A class identifier, should be 00021401-0000-0000-C000-000000000046. 30 | LinkFlags FlagMap // Information about the file and optional sections in the file. 31 | FileAttributes FlagMap // File attributes about link target, originally a uint32. 32 | CreationTime time.Time // Creation time of link target in UTC. 16 bytes in file. 33 | AccessTime time.Time // Access time of link target. Could be zero. 16 bytes in file. 34 | WriteTime time.Time // Write time of link target. Could be zero. 16 bytes in file. 35 | TargetFileSize uint32 // Filesize of link target. If larger than capacity, it will have the LSB 32-bits of size. 36 | IconIndex int32 // 32-bit signed integer, the index of an icon within a given icon location. 37 | ShowCommand string // Result of the uint32 integer: The expected windows state of the target after execution. 38 | HotKey string // HotKeyFlags structure to launch the target. Original is uint16. 39 | Reserved1 uint16 // Zero. 40 | Reserved2 uint32 // Zero. 41 | Reserved3 uint32 // Zero. 42 | Raw []byte // Section's raw bytes. 43 | } 44 | 45 | // linkFlags defines what shell link structures are in the file. 46 | var linkFlags = []string{ 47 | "HasLinkTargetIDList", // bit00 - ShellLinkHeader is followed by a LinkTargetIDList structure. 48 | "HasLinkInfo", // bit01 - LinkInfo in file. 49 | "HasName", // bit02 - NAME_String in file. 50 | "HasRelativePath", // bit03 - RELATIVE_PATH in file. 51 | "HasWorkingDir", // bit04 - WORKING_DIR in file. 52 | "HasArguments", // bit05 - COMMAND_LINE_ARGUMENTS 53 | "HasIconLocation", // bit06 - ICON_LOCATION 54 | "IsUnicode", // bit07 - Strings are in unicode 55 | "ForceNoLinkInfo", // bit08 - LinkInfo is ignored 56 | "HasExpString", // bit09 - The shell link is saved with an EnvironmentVariableDataBlock 57 | "RunInSeparateProcess", // bit10 - Target runs in a 16-bit virtual machine 58 | "Unused1", // bit11 - ignore 59 | "HasDarwinID", // bit12 - The shell link is saved with a DarwinDataBlock 60 | "RunAsUser", // bit13 - The application is run as a different user when the target of the shell link is activated. 61 | "HasExpIcon", // bit14 - The shell link is saved with an IconEnvironmentDataBlock 62 | "NoPidlAlias", // bit15 - The file system location is represented in the shell namespace when the path to an item is parsed into an IDList. 63 | "Unused2", // bit16 - ignore 64 | "RunWithShimLayer", // bit17 - The shell link is saved with a ShimDataBlock. 65 | "ForceNoLinkTrack", // bit18 - The TrackerDataBlock is ignored. 66 | "EnableTargetMetadata", // bit19 - The shell link attempts to collect target properties and store them in the PropertyStoreDataBlock (section 2.5.7) when the link target is set. 67 | "DisableLinkPathTracking", // bit20 - The EnvironmentVariableDataBlock is ignored. 68 | "DisableKnownFolderTracking", // bit21 - The SpecialFolderDataBlock (section 2.5.9) and the KnownFolderDataBlock (section 2.5.6) are ignored when loading the shell link. If this bit is set, these extra data blocks SHOULD NOT be saved when saving the shell link. 69 | "DisableKnownFolderAlias", // bit22 - If the link has a KnownFolderDataBlock (section 2.5.6), the unaliased form of the known folder IDList SHOULD be used when translating the target IDList at the time that the link is loaded. 70 | "AllowLinkToLink", // bit23 - Creating a link that references another link is enabled. Otherwise, specifying a link as the target IDList SHOULD NOT be allowed. 71 | "UnaliasOnSave", // bit24 - When saving a link for which the target IDList is under a known folder, either the unaliased form of that known folder or the target IDList SHOULD be used. 72 | "PreferEnvironmentPath", // bit25 - The target IDList SHOULD NOT be stored; instead, the path specified in the EnvironmentVariableDataBlock (section 2.5.4) SHOULD be used to refer to the target. 73 | "KeepLocalIDListForUNCTarget", // bit26 - When the target is a UNC name that refers to a location on a local machine, the local path IDList in the PropertyStoreDataBlock (section 2.5.7) SHOULD be stored, so it can be used when the link is loaded on the local machine. 74 | } 75 | 76 | // fileAttributesFlags represent target file attributes. 77 | var fileAttributesFlags = []string{ 78 | "FILE_ATTRIBUTE_READONLY", 79 | "FILE_ATTRIBUTE_HIDDEN", 80 | "FILE_ATTRIBUTE_SYSTEM", 81 | "Reserved1", // Must be zero. 82 | "FILE_ATTRIBUTE_DIRECTORY", 83 | "FILE_ATTRIBUTE_ARCHIVE", 84 | "Reserved2", // Must be zero. 85 | "FILE_ATTRIBUTE_NORMAL", 86 | "FILE_ATTRIBUTE_TEMPORARY", 87 | "FILE_ATTRIBUTE_SPARSE_FILE", 88 | "FILE_ATTRIBUTE_REPARSE_POINT", 89 | "FILE_ATTRIBUTE_COMPRESSED", 90 | "FILE_ATTRIBUTE_OFFLINE", 91 | "FILE_ATTRIBUTE_NOT_CONTENT_INDEXED", 92 | "FILE_ATTRIBUTE_ENCRYPTED", 93 | } 94 | 95 | // Header parses the first 0x4c bytes of the io.Reader and returns a ShellLinkHeader. 96 | func Header(r io.Reader, maxSize uint64) (head ShellLinkHeaderSection, err error) { 97 | 98 | // Read the section. 99 | sectionData, sectionReader, sectionSize, err := readSection(r, 4, maxSize) 100 | if err != nil { 101 | return head, fmt.Errorf("lnk.header: error reading magic string - %s", err.Error()) 102 | } 103 | 104 | // Check sectionSize (the first four bytes) against 0x4c. 105 | if sectionSize != headerSize { 106 | return head, fmt.Errorf("lnk.header: invalid magic string - got %x, want %s", sectionSize, "0x4C") 107 | } 108 | head.Magic = uint32(sectionSize) 109 | 110 | // Store raw bytes. 111 | head.Raw = sectionData 112 | 113 | // Next 16 bytes should be 00021401-0000-0000-C000-000000000046. 114 | // When reading, we will see 0114020000000000c000000000000046. 115 | var clsID [16]byte 116 | err = binary.Read(sectionReader, binary.LittleEndian, &clsID) 117 | if err != nil { 118 | return head, fmt.Errorf("lnk.header: error reading LinkCLSID - %s", err.Error()) 119 | } 120 | hexClsID := hex.EncodeToString(clsID[:]) 121 | if hexClsID != classID { 122 | return head, 123 | fmt.Errorf("lnk.header: invalid class ID - got %s, want %s", hexClsID, classID) 124 | } 125 | head.LinkCLSID = clsID 126 | 127 | // Parse LinkFlags. 128 | // Convert the next uint32 to bits, go over the bits and add the flags. 129 | // We will lose preceding zeros by using uint32 but we do not care about them. 130 | var lf uint32 131 | err = binary.Read(sectionReader, binary.LittleEndian, &lf) 132 | if err != nil { 133 | return head, fmt.Errorf("lnk.header: reading LinkFlags - %s", err.Error()) 134 | } 135 | head.LinkFlags = matchFlag(lf, linkFlags) 136 | 137 | // Parse FileAttributes. 138 | var attribs uint32 139 | // Same as before, read BigEndian. 140 | err = binary.Read(sectionReader, binary.LittleEndian, &attribs) 141 | if err != nil { 142 | return head, fmt.Errorf("lnk.header: reading FileAttributes - %s", err.Error()) 143 | } 144 | head.FileAttributes = matchFlag(attribs, fileAttributesFlags) 145 | 146 | // Convert timestamps from Windows Filetime to time.Time. 147 | var crTime, wrTime, acTime [8]byte 148 | err = binary.Read(sectionReader, binary.LittleEndian, &crTime) 149 | if err != nil { 150 | return head, fmt.Errorf("lnk.header: reading CreationTime - %s", err.Error()) 151 | } 152 | head.CreationTime = toTime(crTime) 153 | 154 | err = binary.Read(sectionReader, binary.LittleEndian, &wrTime) 155 | if err != nil { 156 | return head, fmt.Errorf("lnk.header: reading WriteTime - %s", err.Error()) 157 | } 158 | head.WriteTime = toTime(wrTime) 159 | 160 | err = binary.Read(sectionReader, binary.LittleEndian, &acTime) 161 | if err != nil { 162 | return head, fmt.Errorf("lnk.header: reading AccessTime - %s", err.Error()) 163 | } 164 | head.AccessTime = toTime(acTime) 165 | 166 | // Target file size. 167 | err = binary.Read(sectionReader, binary.LittleEndian, &head.TargetFileSize) 168 | if err != nil { 169 | return head, fmt.Errorf("lnk.header: reading target file size - %s", err.Error()) 170 | } 171 | 172 | // Icon index is a signed 32-bit integer. 173 | err = binary.Read(sectionReader, binary.LittleEndian, &head.IconIndex) 174 | if err != nil { 175 | return head, fmt.Errorf("lnk.header: reading icon index - %s", err.Error()) 176 | } 177 | 178 | // ShowCommand 179 | var sw uint32 180 | err = binary.Read(sectionReader, binary.LittleEndian, &sw) 181 | if err != nil { 182 | return head, fmt.Errorf("lnk.header: reading showcommand - %s", err.Error()) 183 | } 184 | head.ShowCommand = showCommand(sw) 185 | 186 | // Hotkey. 187 | var hk uint16 188 | err = binary.Read(sectionReader, binary.LittleEndian, &hk) 189 | if err != nil { 190 | return head, fmt.Errorf("lnk.header: reading hotkey - %s", err.Error()) 191 | } 192 | head.HotKey = HotKey(hk) 193 | 194 | // The rest should be 10 0x00 bytes. 195 | binary.Read(sectionReader, binary.LittleEndian, &head.Reserved1) 196 | binary.Read(sectionReader, binary.LittleEndian, &head.Reserved2) 197 | binary.Read(sectionReader, binary.LittleEndian, &head.Reserved3) 198 | 199 | return head, err 200 | } 201 | 202 | // String prints the ShellLinkHeader in a table. 203 | func (h ShellLinkHeaderSection) String() string { 204 | var sb, flags, attribs strings.Builder 205 | 206 | // Append all flags. 207 | for fl := range h.LinkFlags { 208 | flags.WriteString(fl) 209 | flags.WriteString("\n") 210 | } 211 | 212 | // Append all file attributes. 213 | for at := range h.FileAttributes { 214 | attribs.WriteString(at) 215 | attribs.WriteString("\n") 216 | } 217 | 218 | table := tablewriter.NewWriter(&sb) 219 | table.SetAlignment(tablewriter.ALIGN_LEFT) 220 | table.SetRowLine(true) 221 | 222 | table.SetHeader([]string{"ShellLinkHeader", "Value"}) 223 | 224 | table.Append([]string{"Magic", uint32TableStr(h.Magic)}) 225 | table.Append([]string{"LinkCLSID", hex.EncodeToString(h.LinkCLSID[:])}) 226 | table.Append([]string{"LinkFlags", flags.String()}) 227 | table.Append([]string{"FileAttributes", attribs.String()}) 228 | table.Append([]string{"CreationTime", h.CreationTime.String()}) 229 | table.Append([]string{"AccessTime", h.AccessTime.String()}) 230 | table.Append([]string{"WriteTime", h.WriteTime.String()}) 231 | table.Append([]string{"TargetFileSize", uint32TableStr(h.TargetFileSize)}) 232 | table.Append([]string{"IconIndex", uint32TableStr(uint32(h.IconIndex))}) 233 | table.Append([]string{"HotKey", h.HotKey}) 234 | table.Render() 235 | 236 | return sb.String() 237 | } 238 | 239 | // Dump returns the hex.Dump of section data. 240 | func (h ShellLinkHeaderSection) Dump() string { 241 | return hex.Dump(h.Raw) 242 | } 243 | -------------------------------------------------------------------------------- /headerutils.go: -------------------------------------------------------------------------------- 1 | package lnk 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // ShellLinkHeader utilities. 12 | 13 | /* 14 | ShowCommand valid values: 15 | 0x00000001 - SW_SHOWNORMAL - The application is open and its window is open in a normal fashion. 16 | 0x00000003 - SW_SHOWMAXIMIZED - The application is open, and keyboard focus is given to the application, but its window is not shown. 17 | 0x00000007 - SW_SHOWMINNOACTIVE - The application is open, but its window is not shown. It is not given the keyboard focus. 18 | All other values are SW_SHOWNORMAL. 19 | */ 20 | // showCommand returns the string associated with ShowCommand uint32 field. 21 | func showCommand(s uint32) string { 22 | switch s { 23 | case 0x03: 24 | return "SW_SHOWMAXIMIZED" 25 | case 0x07: 26 | return "SW_SHOWMINNOACTIVE" 27 | } 28 | // Anything other than these two (include 0x01) is "SW_SHOWNORMAL" 29 | return "SW_SHOWNORMAL" 30 | } 31 | 32 | /* 33 | HotKeyFlags contains the hotkey. 34 | Although it's 4 bytes, only the first 2 bytes are used. 35 | 36 | First byte is LowByte. 37 | If between 0x30 and 0x5A inclusive, it's ASCII-hex of the key. 38 | If between 0x70 and 0x87 it's F(num-0x70+1) (e.g. 0x70 == F1 and 0x87 == F24). 39 | 0x90 == NUM LOCK and 0x91 SCROLL LOCK. 40 | 41 | Second byte is HighByte. 42 | 0x01: SHIFT 43 | 0X02: CTRL 44 | 0X03: ALT 45 | */ 46 | // HotKey returns the string representation of the hotkey uint32. 47 | func HotKey(hotkey uint16) string { 48 | var sb strings.Builder 49 | lb := byteMaskuint16(hotkey, 0) // first byte 50 | hb := byteMaskuint16(hotkey, 1) // second byte 51 | 52 | switch hb { 53 | case 0x01: 54 | sb.WriteString("SHIFT") 55 | case 0x02: 56 | sb.WriteString("CTRL") 57 | case 0x04: 58 | sb.WriteString("ALT") 59 | default: 60 | // 0x00 is technically "no key assigned", but any value other than these 61 | // is the same. 62 | return "No Key Assigned" 63 | } 64 | 65 | sb.WriteString("+") 66 | 67 | switch { 68 | case 0x30 <= lb && lb <= 0x5A: 69 | sb.WriteString(string(lb)) 70 | case 0x70 <= lb && lb <= 0x87: 71 | sb.WriteString("F" + strconv.Itoa(int(lb-0x70+1))) 72 | case lb == 0x90: 73 | sb.WriteString("NUM LOCK") 74 | case lb == 0x91: 75 | sb.WriteString("SCROLL LOCK") 76 | default: 77 | // 0x00 is technically "no key assigned", but any value other than these 78 | // is the same. 79 | return "No Key Assigned" 80 | } 81 | return sb.String() 82 | } 83 | 84 | // toTime converts an 8-byte Windows Filetime to time.Time. 85 | func toTime(t [8]byte) time.Time { 86 | // Taken from https://golang.org/src/syscall/types_windows.go#L352, which is only available on Windows 87 | nsec := int64(binary.LittleEndian.Uint32(t[4:]))<<32 + int64(binary.LittleEndian.Uint32(t[:4])) 88 | // change starting time to the Epoch (00:00:00 UTC, January 1, 1970) 89 | nsec -= 116444736000000000 90 | // convert into nanoseconds 91 | nsec *= 100 92 | return time.Unix(0, nsec) 93 | } 94 | 95 | // formatTime converts a 8-byte Windows Filetime to time.Time and then formats 96 | // it to string. 97 | func formatTime(t [8]byte) string { 98 | return toTime(t).Format("2006-01-02 15:04:05.999999 -07:00") 99 | } 100 | 101 | // Flag utilities. 102 | 103 | /* 104 | matchFlag does the following: 105 | Given a uint32 flag read in littleEndian from disk and a []string, 106 | match the flag bits and return a map[string]bool (FlagMap) that has the 107 | // matched flags as keys. 108 | Flag bits must be reversed because bits are matched to the flags from 0 109 | onwards but the bit string is the other way around. 110 | */ 111 | func matchFlag(flag uint32, flagText []string) FlagMap { 112 | // Convert to bits and then reverse. 113 | flagBits := reverse(fmt.Sprintf("%b", flag)) 114 | mp := make(FlagMap, 0) 115 | // If we have more bits than flags (something has gone wrong or the file is corrupted), 116 | // then reduce the flagbits. 117 | 118 | if len(flagBits) > len(flagText) { 119 | flagBits = flagBits[:len(flagText)] 120 | } 121 | for bitIndex := 0; bitIndex < len(flagBits); bitIndex++ { 122 | if flagBits[bitIndex] == 0x31 { 123 | mp[flagText[bitIndex]] = true 124 | } 125 | } 126 | return mp 127 | } 128 | -------------------------------------------------------------------------------- /headerutils_test.go: -------------------------------------------------------------------------------- 1 | package lnk 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestHotKey(t *testing.T) { 8 | type args struct { 9 | hotkey uint16 10 | } 11 | tests := []struct { 12 | name string 13 | args args 14 | want string 15 | }{ 16 | {"shift+0", args{uint16(0x0130)}, "SHIFT+0"}, 17 | {"shift+Z", args{uint16(0x015A)}, "SHIFT+Z"}, 18 | {"invalid-low", args{uint16(0x0101)}, "No Key Assigned"}, 19 | {"invalid-low", args{uint16(0x0001)}, "No Key Assigned"}, 20 | {"invalid-high", args{uint16(0x0035)}, "No Key Assigned"}, 21 | {"invalid-high", args{uint16(0x0535)}, "No Key Assigned"}, 22 | {"alt+F12", args{uint16(0x047B)}, "ALT+F12"}, 23 | {"ctrl+F12", args{uint16(0x027B)}, "CTRL+F12"}, 24 | {"invalid-low-between", args{uint16(0x025B)}, "No Key Assigned"}, 25 | {"invalid-low-between", args{uint16(0x0269)}, "No Key Assigned"}, 26 | {"invalid-low-over", args{uint16(0x0269)}, "No Key Assigned"}, 27 | {"alt+numlock", args{uint16(0x0490)}, "ALT+NUM LOCK"}, 28 | {"shift+scrolllock", args{uint16(0x0191)}, "SHIFT+SCROLL LOCK"}, 29 | {"invalid-low-over", args{uint16(0x01FF)}, "No Key Assigned"}, 30 | {"invalid-both-over", args{uint16(0x10FF)}, "No Key Assigned"}, 31 | } 32 | for _, tt := range tests { 33 | t.Run(tt.name, func(t *testing.T) { 34 | if got := HotKey(tt.args.hotkey); got != tt.want { 35 | t.Errorf("HotKey() = %v, want %v", got, tt.want) 36 | } 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /idlist.go: -------------------------------------------------------------------------------- 1 | package lnk 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | // ItemList structure. 10 | // If Header has HasLinkTargetIDList, then header is immediately followed by 11 | // one LinkTargetIDList structure. 12 | 13 | // LinkTargetIDListSection contains information about the target of the link. 14 | // Section 2.2 in= [MS-SHLLINK] 15 | type LinkTargetIDListSection struct { 16 | // First two bytes is IDListSize. 17 | IDListSize uint16 18 | // // Section's raw bytes. 19 | List IDList 20 | // Raw conta 21 | } 22 | 23 | // IDList represents a persisted item ID list. 24 | type IDList struct { 25 | // ItemIDList contains the IDLists. 26 | ItemIDList []ItemID 27 | // TerminalID is 00 00. 28 | TerminalID uint16 29 | } 30 | 31 | // ItemID is an element from IDList. 32 | // From [MS-SHLLINK]: 33 | // "The data stored in a given ItemID is defined by the source that corresponds 34 | // to the location in the target namespace of the preceding ItemIDs. This data 35 | // uniquely identifies the items in that part of the namespace." 36 | type ItemID struct { 37 | // Size of ItemID INCLUDING the size. 38 | Size uint16 39 | // Data length is size-2 bytes. 40 | Data []byte 41 | } 42 | 43 | // LinkTarget returns a populated LinkTarget based on bytes passed. []byte 44 | // should point to the start of the section. Normally this will be offset 0x4c 45 | // of the lnk file. 46 | func LinkTarget(r io.Reader) (li LinkTargetIDListSection, err error) { 47 | 48 | // Read the first two bytes to get the IDListSize. 49 | err = binary.Read(r, binary.LittleEndian, &li.IDListSize) 50 | if err != nil { 51 | return li, fmt.Errorf("lnk.LinkTarget: read IDListSize - %s", err.Error()) 52 | } 53 | // fmt.Println(li.IDListSize) 54 | 55 | // Instead of reading IDListSize bytes, we read uint16 which is length, if 56 | // this item is zero, we have reached TerminalID which is 00 00. If not, read 57 | // that many bytes. If the file format is wrong, we may bleed into the next 58 | // section, but then again the IDListSize might be wrong too. 59 | 60 | var idList IDList 61 | 62 | // Start populating ItemIDs. 63 | var items []ItemID 64 | var itemSize uint16 65 | for { 66 | err = binary.Read(r, binary.LittleEndian, &itemSize) 67 | if err != nil { 68 | return li, fmt.Errorf("lnk.LinkTarget: read item size - %s", err.Error()) 69 | } 70 | // Check if we have reach the TerminalID 71 | if itemSize == 0 { 72 | idList.TerminalID = itemSize 73 | // fmt.Println("Reached TerminalID") 74 | break 75 | } 76 | // If not, read those many bytes-2. 77 | itemData := make([]byte, itemSize-2) 78 | err = binary.Read(r, binary.LittleEndian, &itemData) 79 | if err != nil { 80 | return li, fmt.Errorf("lnk.LinkTarget: read item data - %s", err.Error()) 81 | } 82 | items = append(items, ItemID{Size: itemSize, Data: itemData}) 83 | } 84 | 85 | // fmt.Println(len(items)) 86 | 87 | // for _, it := range items { 88 | // fmt.Printf("Item Size: %d bytes.\n", it.Size) 89 | // fmt.Println("Item Data:", string(it.Data)) 90 | // fmt.Println("--------------------") 91 | // } 92 | 93 | idList.ItemIDList = items 94 | 95 | li.List = idList 96 | 97 | return li, err 98 | } 99 | -------------------------------------------------------------------------------- /img/example01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parsiya/golnk/740a4c27c4ff809937af0497da020565a3e12113/img/example01.png -------------------------------------------------------------------------------- /img/example02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parsiya/golnk/740a4c27c4ff809937af0497da020565a3e12113/img/example02.png -------------------------------------------------------------------------------- /img/example03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parsiya/golnk/740a4c27c4ff809937af0497da020565a3e12113/img/example03.png -------------------------------------------------------------------------------- /linkinfo.go: -------------------------------------------------------------------------------- 1 | package lnk 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "fmt" 8 | "io" 9 | "strings" 10 | 11 | "github.com/olekukonko/tablewriter" 12 | ) 13 | 14 | // LinkInfoSection represents the LinkInfo structure. Section 2.3 of [MS-SHLLINK]. 15 | // It appears right after LinkTargetIDList if it's in the linkFlags. 16 | type LinkInfoSection struct { 17 | // LinkInfo header section start. 18 | 19 | // Size of the LinkInfo structure. Includes these four bytes. 20 | Size uint32 21 | 22 | // Size of LinkInfo header section. 23 | // If == 0x1c => Offsets to optional fields are not specified. 24 | // If >= 0x24 => Offsets to optional fields are specified. 25 | // Header section include LinkInfoSize and some of the following fields. 26 | LinkInfoHeaderSize uint32 27 | 28 | // Offsets are from the start of LinkInfo structure == start of the io.Reader. 29 | // If an offset is zero, then field does not exist. 30 | 31 | // Flags that specify whether the VolumeID, LocalBasePath, LocalBasePathUnicode, 32 | // and CommonNetworkRelativeLink fields are present in this structure. 33 | // See linkInfoFlags 34 | LinkInfoFlags uint32 35 | 36 | // LinkInfoFlagsStr contains the flags in string format. 37 | LinkInfoFlagsStr []string 38 | 39 | // Offset of VolumeID if VolumeIDAndLocalBasePath is set. 40 | VolumeIDOffset uint32 41 | 42 | // Offset of LocalBasePath if VolumeIDAndLocalBasePath is set. 43 | LocalBasePathOffset uint32 44 | 45 | // Offset of CommonNetworkRelativeLink if CommonNetworkRelativeLinkAndPathSuffix is set. 46 | CommonNetworkRelativeLinkOffset uint32 47 | 48 | // Offset of CommonPathSuffix. 49 | CommonPathSuffixOffset uint32 50 | 51 | // Offset of optional LocalBasePathUnicode and present if VolumeIDAndLocalBasePath is set 52 | // and LinkInfoHeaderSize >= 0x24. 53 | LocalBasePathOffsetUnicode uint32 // Optional 54 | 55 | // Offset of CommonPathSuffixUnicode and present if LinkInfoHeaderSize >= 0x24. 56 | CommonPathSuffixOffsetUnicode uint32 // Optional 57 | 58 | // LinkInfo header section end (I think?). 59 | 60 | // VolumeID present if VolumeIDAndLocalBasePath is set. 61 | VolID VolID 62 | 63 | // Null-terminated string present if VolumeIDAndLocalBasePath is set. 64 | // Combine with CommonPathSuffix to get the full path to target. 65 | LocalBasePath string // Optional 66 | 67 | // Optional CommonNetworkRelativeLink, contains information about network 68 | // location of the target. 69 | NetworkRelativeLink CommonNetworkRelativeLink 70 | 71 | // Null-terminated string. Combine with LocalBasePath to get full path to target. 72 | CommonPathSuffix string // Optional 73 | 74 | // Null-terminated Unicode string to base path. 75 | // Present only VolumeIDAndLocalBasePath is set and LinkInfoHeaderSize >= 0x24. 76 | LocalBasePathUnicode string // Optional 77 | 78 | // Null-terminated Unicode string to common path. 79 | // Present only VolumeIDAndLocalBasePath is set and LinkInfoHeaderSize >= 0x24. 80 | CommonPathSuffixUnicode string // Optional 81 | 82 | // Section's raw bytes. 83 | Raw []byte 84 | } 85 | 86 | // linkInfoFlags defines the LinkInfoFlags. Only the first two bits are used for now. 87 | var linkInfoFlags = []string{ 88 | // If 1, VolumeIDOffset and LocalBasePathOffset point to respective fields. 89 | // If LinkInfoHeaderSize >= 0x24 and LocalBasePathOffsetUnicode is populated. 90 | "VolumeIDAndLocalBasePath", // Bit 0 91 | 92 | // If 1, CommonNetworkRelativeLinkOffset field is populated. 93 | // If 0, offset is zero. 94 | "CommonNetworkRelativeLinkAndPathSuffix", // Bit 1 95 | } 96 | 97 | // LinkInfo reads the io.Reader and returns a populated LinkInfoSection. 98 | func LinkInfo(r io.Reader, maxSize uint64) (info LinkInfoSection, err error) { 99 | 100 | // Parse section. 101 | sectionData, sectionReader, sectionSize, err := readSection(r, 4, maxSize) 102 | if err != nil { 103 | return info, fmt.Errorf("lnk.LinkInfo: section - %s", err.Error()) 104 | } 105 | info.Size = uint32(sectionSize) 106 | 107 | // Save raw bytes. 108 | info.Raw = sectionData 109 | 110 | // fmt.Println("info.Size", info.Size) 111 | 112 | // Read LinkInfoHeaderSize. 113 | err = binary.Read(sectionReader, binary.LittleEndian, &info.LinkInfoHeaderSize) 114 | if err != nil { 115 | return info, fmt.Errorf("lnk.LinkInfo: read LinkInfoHeaderSize - %s", err.Error()) 116 | } 117 | 118 | // // If 0x1C no optional fields. 119 | // // If >= 0x24 offset to optional fields are here. 120 | // optionalHeaderFields := false 121 | // if info.LinkInfoHeaderSize == 0x1c { 122 | // optionalHeaderFields = false 123 | // } 124 | // if info.LinkInfoHeaderSize >= 0x24 { 125 | // optionalHeaderFields = true 126 | // } 127 | 128 | // fmt.Printf("LinkInfoHeaderSize is %x, setting optionalHeaderFields to %v.\n", info.LinkInfoHeaderSize, optionalHeaderFields) 129 | 130 | // Read LinkInfoFlags. 131 | err = binary.Read(sectionReader, binary.LittleEndian, &info.LinkInfoFlags) 132 | if err != nil { 133 | return info, fmt.Errorf("lnk.LinkInfo: read LinkInfoFlags - %s", err.Error()) 134 | } 135 | 136 | // fmt.Println("LinkInfoFlags", info.LinkInfoFlags) 137 | // Set flags. 138 | for bitIndex := 0; bitIndex < 2; bitIndex++ { 139 | if bitMaskuint32(info.LinkInfoFlags, bitIndex) { 140 | info.LinkInfoFlagsStr = append(info.LinkInfoFlagsStr, linkInfoFlags[bitIndex]) 141 | } 142 | } 143 | 144 | // fmt.Println("LinkInfoFlagsStr", info.LinkInfoFlagsStr) 145 | 146 | // Read VolumeIDOffset, LocalBasePathOffset, CommonNetworkRelativeLinkOffset 147 | // and CommonPathSuffixOffset because they are not optional. Then we will 148 | // act based on LinkInfoFlags. 149 | 150 | // Read VolumeIDOffset. 151 | err = binary.Read(sectionReader, binary.LittleEndian, &info.VolumeIDOffset) 152 | if err != nil { 153 | return info, fmt.Errorf("lnk.LinkInfo: read VolumeIDOffset - %s", err.Error()) 154 | } 155 | // fmt.Printf("VolumeIDOffset : %v\n", info.VolumeIDOffset) 156 | 157 | // Read LocalBasePathOffset. 158 | err = binary.Read(sectionReader, binary.LittleEndian, &info.LocalBasePathOffset) 159 | if err != nil { 160 | return info, fmt.Errorf("lnk.LinkInfo: read LocalBasePathOffset - %s", err.Error()) 161 | } 162 | // fmt.Println("LocalBasePathOffset:", info.LocalBasePathOffset) 163 | 164 | // Read CommonNetworkRelativeLinkOffset. 165 | err = binary.Read(sectionReader, binary.LittleEndian, &info.CommonNetworkRelativeLinkOffset) 166 | if err != nil { 167 | return info, fmt.Errorf("lnk.LinkInfo: read CommonNetworkRelativeLinkOffset - %s", err.Error()) 168 | } 169 | // fmt.Println("CommonNetworkRelativeLinkOffset:", info.CommonNetworkRelativeLinkOffset) 170 | 171 | // Read CommonPathSuffixOffset. 172 | err = binary.Read(sectionReader, binary.LittleEndian, &info.CommonPathSuffixOffset) 173 | if err != nil { 174 | return info, fmt.Errorf("lnk.LinkInfo: read CommonPathSuffixOffset - %s", err.Error()) 175 | } 176 | // fmt.Println("CommonPathSuffixOffset:", info.CommonPathSuffixOffset) 177 | 178 | // Read CommonPathSuffix if offset is not zero. 179 | if info.CommonPathSuffixOffset != 0x00 { 180 | info.CommonPathSuffix = readString(sectionData[info.CommonPathSuffixOffset:]) 181 | } 182 | 183 | // If VolumeIDAndLocalBasePath is set then VolumeIDOffset and LocalBasePathOffset 184 | // are both set. 185 | if bitMaskuint32(info.LinkInfoFlags, 0) { 186 | // Populate VolumeID based on offset from linkInfo. 187 | if info.VolumeIDOffset > info.Size { 188 | return info, 189 | fmt.Errorf("lnk.LinkInfo: VolumeIDOffset %d larger than LinkInfo size %d", 190 | info.VolumeIDOffset, info.Size) 191 | } 192 | 193 | // Read VolumeID struct from offset. 194 | // Make an io.Reader for bytes starting from that offset. 195 | vbuf := bytes.NewReader(sectionData[info.VolumeIDOffset:]) 196 | vol, err := VolumeID(vbuf, maxSize) 197 | if err != nil { 198 | return info, fmt.Errorf("lnk.LinkInfo: parse VolumeID - %s", err.Error()) 199 | } 200 | info.VolID = vol 201 | // fmt.Println(StructToJSON(info.VolID, true)) 202 | 203 | // Read LocalBasePath which is a null-terminated string. 204 | info.LocalBasePath = readString(sectionData[info.LocalBasePathOffset:]) 205 | // fmt.Println("LocalBasePath", info.LocalBasePath) 206 | 207 | // LocalBasePathOffsetUnicode and CommonPathSuffixOffsetUnicode only 208 | // exist if LinkInfoHeaderSize >= 0x24 and are not zero if 209 | // VolumeIDAndLocalBasePath is set. 210 | // TODO: Find lnk files that test this. 211 | if info.LinkInfoHeaderSize >= 0x24 { 212 | // Read LocalBasePathOffsetUnicode. 213 | err = binary.Read(sectionReader, binary.LittleEndian, &info.LocalBasePathOffsetUnicode) 214 | if err != nil { 215 | return info, fmt.Errorf("lnk.LinkInfo: read LocalBasePathOffsetUnicode - %s", err.Error()) 216 | } 217 | // fmt.Println("LocalBasePathOffsetUnicode:", info.LocalBasePathOffsetUnicode) 218 | 219 | // If we have reached here, it's non-zero, so try and read it, if the 220 | // offset is not larger than section. 221 | if uint32(sectionSize) > info.LocalBasePathOffsetUnicode && info.LocalBasePathOffsetUnicode != 0x00 { 222 | // Read unicode string. 223 | info.LocalBasePathUnicode = readUnicodeString(sectionData[info.LocalBasePathOffsetUnicode:]) 224 | } 225 | // fmt.Println("LocalBasePathUnicode:", info.LocalBasePathUnicode) 226 | 227 | // Read CommonPathSuffixOffsetUnicode. 228 | err = binary.Read(sectionReader, binary.LittleEndian, &info.CommonPathSuffixOffsetUnicode) 229 | if err != nil { 230 | return info, fmt.Errorf("lnk.LinkInfo: read CommonPathSuffixOffsetUnicode - %s", err.Error()) 231 | } 232 | // fmt.Println("CommonPathSuffixOffsetUnicode:", info.CommonPathSuffixOffsetUnicode) 233 | 234 | // Read it. 235 | if uint32(sectionSize) > info.CommonPathSuffixOffsetUnicode && info.CommonPathSuffixOffsetUnicode != 0x00 { 236 | // Read unicode string. 237 | info.CommonPathSuffixUnicode = readUnicodeString(sectionData[info.CommonPathSuffixOffsetUnicode:]) 238 | } 239 | // fmt.Println("CommonPathSuffixUnicode:", info.CommonPathSuffixUnicode) 240 | } 241 | } 242 | 243 | // Check if CommonNetworkRelativeLinkAndPathSuffix flag is set. 244 | if bitMaskuint32(info.LinkInfoFlags, 1) { 245 | 246 | // Read and parse CommonNetworkRelativeLink, if it exists. It exists if the 247 | // CommonNetworkRelativeLinkAndPathSuffix is set and the offset is not zero. 248 | // TODO: Find lnks that have this to test. 249 | if info.CommonNetworkRelativeLinkOffset != 0x00 { 250 | // Create a reader from CommonNetworkRelativeLink data. 251 | nbuf := bytes.NewReader(sectionData[info.CommonNetworkRelativeLinkOffset:]) 252 | // And parse it. 253 | info.NetworkRelativeLink, _ = CommonNetwork(nbuf, maxSize) 254 | } 255 | } 256 | return info, err 257 | } 258 | 259 | // String prints LinkInfoSection in a table. 260 | func (li LinkInfoSection) String() string { 261 | 262 | var sb, flags strings.Builder 263 | // Append all flags. 264 | for _, fl := range li.LinkInfoFlagsStr { 265 | flags.WriteString(fl) 266 | flags.WriteString("\n") 267 | } 268 | 269 | table := tablewriter.NewWriter(&sb) 270 | table.SetAlignment(tablewriter.ALIGN_LEFT) 271 | table.SetRowLine(true) 272 | 273 | table.SetHeader([]string{"LinkInfo", "Value"}) 274 | 275 | table.Append([]string{"Size", uint32TableStr(li.Size)}) 276 | table.Append([]string{"HeaderSize", uint32TableStr(li.LinkInfoHeaderSize)}) 277 | table.Append([]string{"Flags", flags.String()}) 278 | 279 | // Only add rows that exist (their offset is not zero). 280 | if li.LocalBasePathOffset != 0 { 281 | table.Append([]string{"LocalBasePathOffset", uint32TableStr(li.LocalBasePathOffset)}) 282 | table.Append([]string{"LocalBasePath", li.LocalBasePath}) 283 | } 284 | 285 | if li.CommonPathSuffixOffset != 0 { 286 | table.Append([]string{"CommonPathSuffixOffset", uint32TableStr(li.CommonPathSuffixOffset)}) 287 | table.Append([]string{"CommonPathSuffix", li.CommonPathSuffix}) 288 | } 289 | 290 | if li.LocalBasePathOffsetUnicode != 0 { 291 | table.Append([]string{"LocalBasePathOffsetUnicode", uint32TableStr(li.LocalBasePathOffsetUnicode)}) 292 | table.Append([]string{"LocalBasePathUnicode", li.LocalBasePathUnicode}) 293 | } 294 | 295 | if li.CommonPathSuffixOffsetUnicode != 0 { 296 | table.Append([]string{"CommonPathSuffixOffsetUnicode", uint32TableStr(li.CommonPathSuffixOffsetUnicode)}) 297 | table.Append([]string{"CommonPathSuffixUnicode", li.CommonPathSuffixUnicode}) 298 | } 299 | 300 | // Add VolumeID and CommonNetwork offsets if they are not zero. 301 | if li.VolumeIDOffset != 0 { 302 | table.Append([]string{"VolumeIDOffset", uint32TableStr(li.VolumeIDOffset)}) 303 | } 304 | 305 | if li.CommonNetworkRelativeLinkOffset != 0 { 306 | table.Append([]string{"CommonNetworkRelativeLinkOffset", uint32TableStr(li.CommonNetworkRelativeLinkOffset)}) 307 | } 308 | 309 | table.Render() 310 | 311 | // Print VolumeID in a separate table if it exists. 312 | if li.VolumeIDOffset != 0 { 313 | sb.WriteString("\n\n") 314 | sb.WriteString(li.VolID.String()) 315 | } 316 | 317 | // Print CommonNetworkRelativeLink in a separate table if it exists. 318 | if li.CommonNetworkRelativeLinkOffset != 0 { 319 | sb.WriteString("\n\n") 320 | sb.WriteString(li.NetworkRelativeLink.String()) 321 | } 322 | return sb.String() 323 | } 324 | 325 | // Dump returns the hex.Dump of section data. 326 | func (li LinkInfoSection) Dump() string { 327 | return hex.Dump(li.Raw) 328 | } 329 | -------------------------------------------------------------------------------- /linkinfo_network.go: -------------------------------------------------------------------------------- 1 | package lnk 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | "strings" 8 | 9 | "github.com/olekukonko/tablewriter" 10 | ) 11 | 12 | // CommonNetworkRelativeLink (section 2.3.2) 13 | // Information about the network location where a link target is stored, 14 | type CommonNetworkRelativeLink struct { 15 | Size uint32 16 | 17 | // Only the first two bits are used. commonNetworkRelativeLinkFlags 18 | CommonNetworkRelativeLinkFlags uint32 19 | 20 | // String version of the flag. 21 | CommonNetworkRelativeLinkFlagsStr []string 22 | 23 | // Offset of NetName field from start of structure. 24 | // If value > 0x14, then NetNameOffsetUnicode must not exist. 25 | NetNameOffset uint32 26 | 27 | // Offset of DeviceName field from start of structure. 28 | DeviceNameOffset uint32 29 | 30 | // Type of NetworkProvider. See networkProviderType for table. 31 | // If ValidNetType is not set, ignore this. 32 | // NetworkProviderType uint32 33 | NetworkProviderType string // A uint32 in file, maps to networkProviderType. 34 | 35 | // Optional offset of NetNameUnicode. Must not exist if NetNameOffset > 0x14. 36 | NetNameOffsetUnicode uint32 37 | 38 | // Optional value of DeviceNameUnicode. Must not exist if NetNameOffset > 0x14. 39 | DeviceNameOffsetUnicode uint32 40 | 41 | // Server share path (e.g. \\server\share). Null-terminated string. 42 | NetName string 43 | 44 | // Device name like drive letter. Null-terminated string. 45 | DeviceName string 46 | 47 | // Unicode string. Must not exist if NetNameOffset > 0x14. 48 | NetNameUnicode string 49 | 50 | // Unicode string. Must not exist if NetNameOffset > 0x14. 51 | DeviceNameUnicode string 52 | } 53 | 54 | // commonNetworkRelativeLinkFlags is the index for CommonNetworkRelativeLinkFlags. 55 | var commonNetworkRelativeLinkFlags = []string{ 56 | // If 1, DeviceNameOffset is offset to device name. 57 | // If 0, DeviceNameOffset must be zero. 58 | "ValidDevice", // Bit 0 59 | 60 | // If 1, NetProviderType has network provider type. 61 | // If 0, NetProviderType must be zero. 62 | "ValidNetType", // Bit 1 63 | } 64 | 65 | // networkProviderType returns a string representing the network provider based 66 | // on the value of the NetworkProviderType uint32 and "" for invalid values. 67 | func networkProviderType(index uint32) string { 68 | networkMap := map[uint32]string{ 69 | 0x001A0000: "WNNC_NET_AVID", 70 | 0x001B0000: "WNNC_NET_DOCUSPACE", 71 | 0x001C0000: "WNNC_NET_MANGOSOFT", 72 | 0x001D0000: "WNNC_NET_SERNET", 73 | 0X001E0000: "WNNC_NET_RIVERFRONT1", 74 | 0x001F0000: "WNNC_NET_RIVERFRONT2", 75 | 0x00200000: "WNNC_NET_DECORB", 76 | 0x00210000: "WNNC_NET_PROTSTOR", 77 | 0x00220000: "WNNC_NET_FJ_REDIR", 78 | 0x00230000: "WNNC_NET_DISTINCT", 79 | 0x00240000: "WNNC_NET_TWINS", 80 | 0x00250000: "WNNC_NET_RDR2SAMPLE", 81 | 0x00260000: "WNNC_NET_CSC", 82 | 0x00270000: "WNNC_NET_3IN1", 83 | 0x00290000: "WNNC_NET_EXTENDNET", 84 | 0x002A0000: "WNNC_NET_STAC", 85 | 0x002B0000: "WNNC_NET_FOXBAT", 86 | 0x002C0000: "WNNC_NET_YAHOO", 87 | 0x002D0000: "WNNC_NET_EXIFS", 88 | 0x002E0000: "WNNC_NET_DAV", 89 | 0x002F0000: "WNNC_NET_KNOWARE", 90 | 0x00300000: "WNNC_NET_OBJECT_DIRE", 91 | 0x00310000: "WNNC_NET_MASFAX", 92 | 0x00320000: "WNNC_NET_HOB_NFS", 93 | 0x00330000: "WNNC_NET_SHIVA", 94 | 0x00340000: "WNNC_NET_IBMAL", 95 | 0x00350000: "WNNC_NET_LOCK", 96 | 0x00360000: "WNNC_NET_TERMSRV", 97 | 0x00370000: "WNNC_NET_SRT", 98 | 0x00380000: "WNNC_NET_QUINCY", 99 | 0x00390000: "WNNC_NET_OPENAFS", 100 | 0X003A0000: "WNNC_NET_AVID1", 101 | 0x003B0000: "WNNC_NET_DFS", 102 | 0x003C0000: "WNNC_NET_KWNP", 103 | 0x003D0000: "WNNC_NET_ZENWORKS", 104 | 0x003E0000: "WNNC_NET_DRIVEONWEB", 105 | 0x003F0000: "WNNC_NET_VMWARE", 106 | 0x00400000: "WNNC_NET_RSFX", 107 | 0x00410000: "WNNC_NET_MFILES", 108 | 0x00420000: "WNNC_NET_MS_NFS", 109 | 0x00430000: "WNNC_NET_GOOGLE", 110 | } 111 | val, exists := networkMap[index] 112 | if exists { 113 | return val 114 | } 115 | // If not found, return the hex encoded string. 116 | return uint32StrHex(index) 117 | } 118 | 119 | // CommonNetwork reads the section data and populates a CommonNetworkRelativeLink. 120 | // Section 2.3.2 in docs. 121 | func CommonNetwork(r io.Reader, maxSize uint64) (c CommonNetworkRelativeLink, err error) { 122 | // Read the section. 123 | sectionData, sectionReader, sectionSize, err := readSection(r, 4, maxSize) 124 | if err != nil { 125 | return c, fmt.Errorf("golnk.CommonNetwork: read CommonNetwork section - %s", err.Error()) 126 | } 127 | c.Size = uint32(sectionSize) 128 | 129 | // fmt.Println("------") 130 | 131 | // fmt.Printf("Read section CommonNetwork. %d bytes.\n", sectionSize) 132 | 133 | // fmt.Println(hex.Dump(sectionData)) 134 | 135 | // Read CommonNetworkRelativeLinkFlags. 136 | err = binary.Read(sectionReader, binary.LittleEndian, &c.CommonNetworkRelativeLinkFlags) 137 | if err != nil { 138 | return c, fmt.Errorf("golnk.CommonNetwork: read CommonNetworkRelativeLinkFlags - %s", err.Error()) 139 | } 140 | // fmt.Println("CommonNetworkRelativeLinkFlags", c.CommonNetworkRelativeLinkFlags) 141 | 142 | // Parse the flag. 143 | for bitIndex := 0; bitIndex < len(commonNetworkRelativeLinkFlags); bitIndex++ { 144 | if bitMaskuint32(c.CommonNetworkRelativeLinkFlags, bitIndex) { 145 | c.CommonNetworkRelativeLinkFlagsStr = 146 | append(c.CommonNetworkRelativeLinkFlagsStr, commonNetworkRelativeLinkFlags[bitIndex]) 147 | } 148 | } 149 | // fmt.Println("c.CommonNetworkRelativeLinkFlagsStr", c.CommonNetworkRelativeLinkFlagsStr) 150 | 151 | // Read NetNameOffset. 152 | err = binary.Read(sectionReader, binary.LittleEndian, &c.NetNameOffset) 153 | if err != nil { 154 | return c, fmt.Errorf("golnk.CommonNetwork: read NetNameOffset - %s", err.Error()) 155 | } 156 | // fmt.Println("NetNameOffset", c.NetNameOffset) 157 | 158 | // Read DeviceNameOffset. 159 | err = binary.Read(sectionReader, binary.LittleEndian, &c.DeviceNameOffset) 160 | if err != nil { 161 | return c, fmt.Errorf("golnk.CommonNetwork: read DeviceNameOffset - %s", err.Error()) 162 | } 163 | // fmt.Println("DeviceNameOffset", c.DeviceNameOffset) 164 | 165 | // Read NetworkProviderType. 166 | var nType uint32 167 | err = binary.Read(sectionReader, binary.LittleEndian, &nType) 168 | if err != nil { 169 | return c, fmt.Errorf("golnk.CommonNetwork: read NetworkProviderType - %s", err.Error()) 170 | } 171 | // fmt.Println("nType", nType) 172 | // fmt.Printf("%x\n", nType) 173 | 174 | // Map nType to networkProviderType. 175 | c.NetworkProviderType = networkProviderType(nType) 176 | // fmt.Println("networkProviderType", c.NetworkProviderType) 177 | 178 | // If value of NetNameOffset field > 0x14, two optional fields are present: 179 | // NetNameOffsetUnicode - uint32 180 | // DeviceNameOffsetUnicode - uint32 181 | if c.NetNameOffset > 0x14 { 182 | // Read NetNameOffsetUnicode. 183 | err = binary.Read(sectionReader, binary.LittleEndian, &c.NetNameOffsetUnicode) 184 | if err != nil { 185 | return c, fmt.Errorf("golnk.CommonNetwork: read NetNameOffsetUnicode - %s", err.Error()) 186 | } 187 | // fmt.Println("NetNameOffsetUnicode", c.NetNameOffsetUnicode) 188 | 189 | // Read DeviceNameOffsetUnicode. 190 | err = binary.Read(sectionReader, binary.LittleEndian, &c.DeviceNameOffsetUnicode) 191 | if err != nil { 192 | return c, fmt.Errorf("golnk.CommonNetwork: read DeviceNameOffsetUnicode - %s", err.Error()) 193 | } 194 | // fmt.Println("DeviceNameOffsetUnicode", c.DeviceNameOffsetUnicode) 195 | } else { 196 | // fmt.Printf("NetNameOffset 0x%x smaller than or equal to 0x14, skipping Unicode fields.\n", c.NetNameOffset) 197 | // Read NetName from NetNameOffset as a null-terminated string. 198 | c.NetName = readString(sectionData[c.NetNameOffset:]) 199 | // fmt.Println("NetName", c.NetName) 200 | } 201 | return c, err 202 | } 203 | 204 | // String prints CommonNetworkRelativeLink in a table. 205 | func (c CommonNetworkRelativeLink) String() string { 206 | var sb, flags strings.Builder 207 | 208 | // Append all flags. 209 | for _, fl := range c.CommonNetworkRelativeLinkFlagsStr { 210 | flags.WriteString(fl) 211 | flags.WriteString("\n") 212 | } 213 | 214 | table := tablewriter.NewWriter(&sb) 215 | table.SetAlignment(tablewriter.ALIGN_LEFT) 216 | table.SetRowLine(true) 217 | 218 | table.SetHeader([]string{"CommonNetworkRelativeLink", "Value"}) 219 | 220 | table.Append([]string{"Size", uint32TableStr(c.Size)}) 221 | table.Append([]string{"Flags", flags.String()}) 222 | table.Append([]string{"NetworkProviderType", c.NetworkProviderType}) 223 | 224 | // Only add rows that exist (their offset is not zero). 225 | if c.NetNameOffset != 0 { 226 | table.Append([]string{"NetNameOffset", uint32TableStr(c.NetNameOffset)}) 227 | table.Append([]string{"NetName", c.NetName}) 228 | } 229 | 230 | if c.DeviceNameOffset != 0 { 231 | table.Append([]string{"DeviceNameOffset", uint32TableStr(c.DeviceNameOffset)}) 232 | table.Append([]string{"DeviceName", c.DeviceName}) 233 | } 234 | 235 | if c.NetNameOffsetUnicode != 0 { 236 | table.Append([]string{"NetNameOffsetUnicode", uint32TableStr(c.NetNameOffsetUnicode)}) 237 | table.Append([]string{"NetNameUnicode", c.NetNameUnicode}) 238 | } 239 | 240 | if c.DeviceNameOffsetUnicode != 0 { 241 | table.Append([]string{"DeviceNameOffsetUnicode", uint32TableStr(c.DeviceNameOffsetUnicode)}) 242 | table.Append([]string{"DeviceNameUnicode", c.DeviceNameUnicode}) 243 | } 244 | 245 | table.Render() 246 | return sb.String() 247 | } 248 | -------------------------------------------------------------------------------- /linkinfo_volumeid.go: -------------------------------------------------------------------------------- 1 | package lnk 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/hex" 6 | "fmt" 7 | "io" 8 | "strings" 9 | 10 | "github.com/olekukonko/tablewriter" 11 | ) 12 | 13 | // VolID is VolumeID (section 2.3.1. of [SHLLINK]). 14 | // Information about the volume that the link target was on when the link was created. 15 | type VolID struct { 16 | // Size of VolumeID including this. 17 | Size uint32 18 | 19 | // Type of drive that target was stored on. 20 | DriveType string // Originally a uint32 that is the index to the driveType []string. 21 | 22 | // Serial number of the volume. TODO: Store as hex string? 23 | DriveSerialNumber string // Originally a uint32, converted to hex string. 24 | 25 | // Offset to the a null-terminated string that contains the volume label of 26 | // the drive that the link target is stored on. 27 | // If == 0x14, it must be ignored and VolumeLabelOffsetUnicode must be used. 28 | VolumeLabelOffset uint32 29 | 30 | // Offset to Unicode version of VolumeLabel. 31 | // Must not be present if VolumeLabelOffset is not 0x14. 32 | VolumeLabelOffsetUnicode uint32 33 | 34 | // VolumeLabel in either ASCII-HEX or Unicode. 35 | VolumeLabel string 36 | } 37 | 38 | // Different DriveTypes. The value of field is the index to this slice. 39 | var driveType = []string{ 40 | "DRIVE_UNKNOWN", 41 | "DRIVE_NO_ROOT_DIR", 42 | "DRIVE_REMOVABLE", 43 | "DRIVE_FIXED", 44 | "DRIVE_REMOTE", 45 | "DRIVE_CDROM", 46 | "DRIVE_RAMDISK", 47 | } 48 | 49 | // VolumeID reads the VolID struct. 50 | func VolumeID(r io.Reader, maxSize uint64) (v VolID, err error) { 51 | // Read the section. 52 | sectionData, sectionReader, sectionSize, err := readSection(r, 4, maxSize) 53 | if err != nil { 54 | return v, fmt.Errorf("golnk.VolumeID: read VolumeID section - %s", err.Error()) 55 | } 56 | _ = sectionSize 57 | // fmt.Printf("Read section volumeID. %d bytes.\n", sectionSize) 58 | // fmt.Println(hex.Dump(sectionData)) 59 | 60 | // Read DriveType. 61 | var dt uint32 62 | err = binary.Read(sectionReader, binary.LittleEndian, &dt) 63 | if err != nil { 64 | return v, fmt.Errorf("golnk.VolumeID: read VolumeID.DriveType - %s", err.Error()) 65 | } 66 | // Check if it's a valid DriveType. 67 | if dt >= uint32(len(driveType)) { 68 | // This is not in the specification but it's better than just returning 69 | // an error and cancelling the parse. 70 | v.DriveType = "DRIVE_INVALID" 71 | } else { 72 | v.DriveType = driveType[dt] 73 | } 74 | // fmt.Println("VolumeID.DriveType:", v.DriveType) 75 | 76 | // Read DriveSerialNumber which is a uint32. 77 | var sr [4]byte 78 | err = binary.Read(sectionReader, binary.LittleEndian, &sr) 79 | if err != nil { 80 | return v, fmt.Errorf("golnk.VolumeID: read VolumeID.DriveSerialNumber - %s", err.Error()) 81 | } 82 | v.DriveSerialNumber = "0x" + hex.EncodeToString(sr[:]) 83 | 84 | // fmt.Println("VolumeID.DriveSerialNumber:", v.DriveSerialNumber) 85 | 86 | // Read VolumeLabelOffset. 87 | err = binary.Read(sectionReader, binary.LittleEndian, &v.VolumeLabelOffset) 88 | if err != nil { 89 | return v, fmt.Errorf("golnk.VolumeID: read VolumeID.VolumeLabelOffset - %s", err.Error()) 90 | } 91 | // fmt.Println("VolumeID.VolumeLabelOffset:", v.VolumeLabelOffset) 92 | 93 | // Check if it's 0x14, if it's not, then use this offset and read a 94 | // null-terminated string. 95 | // If it is 0x14, ignore this and read the next uint32 for VolumeLabelOffsetUnicode. 96 | if v.VolumeLabelOffset != 0x14 { 97 | // Read a null-terminated string from sectionData[v.VolumeLabelOffset:]. 98 | str := readString(sectionData[v.VolumeLabelOffset:]) 99 | v.VolumeLabel = str 100 | // fmt.Println("VolumeLabel", str) 101 | 102 | // Because we read VolumeLabel manually, VolumeLabelOffsetUnicode must 103 | // not exist and we can return. 104 | return v, nil 105 | } 106 | 107 | // TODO: Test this. 108 | // If v.VolumeLabelOffset is 0x14, it means we need to read a uint32 109 | // to get VolumeLabelOffsetUnicode and read a unicode string there. 110 | err = binary.Read(sectionReader, binary.LittleEndian, &v.VolumeLabelOffsetUnicode) 111 | // fmt.Println("v.VolumeLabelOffsetUnicode", v.VolumeLabelOffsetUnicode) 112 | 113 | // Read a unicode string from that offset. 114 | unicodeStr := readUnicodeString(sectionData[v.VolumeLabelOffsetUnicode:]) 115 | v.VolumeLabel = unicodeStr 116 | // fmt.Println("VolumeLabelUnicode", v.VolumeLabel) 117 | 118 | return v, err 119 | } 120 | 121 | // String prints VolumeID in a table. 122 | func (v VolID) String() string { 123 | 124 | var sb strings.Builder 125 | 126 | table := tablewriter.NewWriter(&sb) 127 | table.SetAlignment(tablewriter.ALIGN_LEFT) 128 | table.SetRowLine(true) 129 | 130 | table.SetHeader([]string{"VolumeID", "Value"}) 131 | 132 | table.Append([]string{"Size", uint32TableStr(v.Size)}) 133 | table.Append([]string{"DriveType", v.DriveType}) 134 | table.Append([]string{"DriveSerialNumber", v.DriveSerialNumber}) 135 | 136 | if v.VolumeLabelOffset != 0 { 137 | table.Append([]string{"VolumeLabelOffset", uint32TableStr(v.VolumeLabelOffset)}) 138 | table.Append([]string{"VolumeLabel", v.VolumeLabel}) 139 | } 140 | 141 | if v.VolumeLabelOffsetUnicode != 0 { 142 | table.Append([]string{"VolumeLabelOffsetUnicode", uint32TableStr(v.VolumeLabelOffsetUnicode)}) 143 | table.Append([]string{"VolumeLabel", v.VolumeLabel}) 144 | } 145 | 146 | table.Render() 147 | return sb.String() 148 | } 149 | -------------------------------------------------------------------------------- /stringdata.go: -------------------------------------------------------------------------------- 1 | package lnk 2 | 3 | import ( 4 | "io" 5 | "strings" 6 | 7 | "github.com/olekukonko/tablewriter" 8 | ) 9 | 10 | // Optional StringData - Section 2.4. 11 | 12 | // StringDataSection represents section 2.4 of the lnk. 13 | // Fields are mostly optional and present if certain flags are set. 14 | type StringDataSection struct { 15 | 16 | // On disk, all these fields have a uint16 size, followed by a string. 17 | // The string is not null-terminated. 18 | // The strings are unicode if IsUnicode is set in header. 19 | 20 | // NameString specifies a description of the shortcut that is displayed to 21 | // end users to identify the purpose of the shell link. 22 | // Present with HasName flag. 23 | NameString string 24 | 25 | // RelativePath specifies the location of the link target relative to the 26 | // file that contains the shell link. 27 | // Present with HasRelativePath flag. 28 | RelativePath string 29 | 30 | // WorkingDir specifies the file system path of the working directory to 31 | // be used when activating the link target. 32 | // Present with HasWorkingDir flag. 33 | WorkingDir string 34 | 35 | // CommandLineArguments stores the command-line arguments of the link target. 36 | // Present with HasArguments flag. 37 | CommandLineArguments string 38 | 39 | // IconLocation specifies the location of the icon to be used. 40 | // Present with HasIconLocation flag. 41 | IconLocation string 42 | } 43 | 44 | // StringData parses the StringData portion of the lnk. 45 | // flags is the ShellLinkHeader.LinkFlags. 46 | func StringData(r io.Reader, linkFlags FlagMap) (st StringDataSection, err error) { 47 | 48 | // Read unicode strings if is unicode flag is set. 49 | isUnicode := linkFlags["IsUnicode"] 50 | 51 | // Read NameString if HasName flag is set. 52 | if linkFlags["HasName"] { 53 | name, _ := readStringData(r, isUnicode) 54 | // fmt.Println("Name", name) 55 | st.NameString = name 56 | } 57 | 58 | // Read NameString if HasName flag is set. 59 | if linkFlags["HasRelativePath"] { 60 | st.RelativePath, err = readStringData(r, isUnicode) 61 | if err != nil { 62 | return st, err 63 | } 64 | } 65 | 66 | // Read WorkingDir if HasWorkingDir flag is set. 67 | if linkFlags["HasWorkingDir"] { 68 | st.WorkingDir, err = readStringData(r, isUnicode) 69 | if err != nil { 70 | return st, err 71 | } 72 | } 73 | 74 | // Read CommandLineArguments if HasArguments flag is set. 75 | if linkFlags["HasArguments"] { 76 | st.CommandLineArguments, err = readStringData(r, isUnicode) 77 | if err != nil { 78 | return st, err 79 | } 80 | } 81 | 82 | // Read IconLocation if HasIconLocation flag is set. 83 | if linkFlags["HasIconLocation"] { 84 | st.IconLocation, err = readStringData(r, isUnicode) 85 | if err != nil { 86 | return st, err 87 | } 88 | } 89 | return st, err 90 | } 91 | 92 | // String prints StringDataSection in a table. 93 | func (st StringDataSection) String() string { 94 | var sb strings.Builder 95 | 96 | table := tablewriter.NewWriter(&sb) 97 | table.SetAlignment(tablewriter.ALIGN_LEFT) 98 | table.SetRowLine(true) 99 | 100 | table.SetHeader([]string{"StringData", "Value"}) 101 | 102 | if st.NameString != "" { 103 | table.Append([]string{"NameString", st.NameString}) 104 | } 105 | 106 | if st.RelativePath != "" { 107 | table.Append([]string{"RelativePath", st.RelativePath}) 108 | } 109 | 110 | if st.WorkingDir != "" { 111 | table.Append([]string{"WorkingDir", st.WorkingDir}) 112 | } 113 | 114 | if st.CommandLineArguments != "" { 115 | table.Append([]string{"CommandLineArguments", st.CommandLineArguments}) 116 | } 117 | 118 | if st.IconLocation != "" { 119 | table.Append([]string{"IconLocation", st.IconLocation}) 120 | } 121 | 122 | table.Render() 123 | return sb.String() 124 | } 125 | 126 | // toASCII converts English Unicode text to ASCII. First we detect if input is 127 | // English text by checking if half of the string is 0x00, if so, remove those bytes. 128 | func toASCII(str string) string { 129 | return strings.Replace(str, " ", "", -1) 130 | } 131 | -------------------------------------------------------------------------------- /test/Visual Studio Code.lnk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parsiya/golnk/740a4c27c4ff809937af0497da020565a3e12113/test/Visual Studio Code.lnk -------------------------------------------------------------------------------- /test/Windows Store.lnk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parsiya/golnk/740a4c27c4ff809937af0497da020565a3e12113/test/Windows Store.lnk -------------------------------------------------------------------------------- /test/funcs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // ReadLittleEndian 4 | -------------------------------------------------------------------------------- /test/m2.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/parsiya/golnk" 7 | ) 8 | 9 | func main() { 10 | 11 | Lnk, err := lnk.File("vbox-svr-win10.lnk") 12 | if err != nil { 13 | panic(err) 14 | } 15 | 16 | // Print header. 17 | fmt.Println(Lnk.Header) 18 | 19 | // Print LocalBasePath. 20 | fmt.Println("BasePath", Lnk.LinkInfo.LocalBasePath) 21 | 22 | fmt.Println(Lnk.LinkInfo) 23 | 24 | fmt.Println(Lnk.StringData) 25 | 26 | fmt.Println(Lnk.DataBlocks) 27 | } 28 | -------------------------------------------------------------------------------- /test/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | lnk "github.com/parsiya/golnk" 8 | ) 9 | 10 | func main() { 11 | 12 | fi, err := os.Open("remote.directory.xp.test") 13 | if err != nil { 14 | panic(err) 15 | } 16 | defer fi.Close() 17 | 18 | // // lnk files are small-ish, no reason not to read everything at once. 19 | // // lnkBytes, err := ioutil.ReadAll(fi) 20 | // // if err != nil { 21 | // // panic(err) 22 | // // } 23 | // // fmt.Printf("Read %d bytes.\n", len(lnkBytes)) 24 | 25 | // h, err := lnk.Header(fi) 26 | // if err != nil { 27 | // panic(err) 28 | // } 29 | // _ = h 30 | 31 | // // fmt.Println(lnk.StructToJSON(h, true)) 32 | // fmt.Println(h) 33 | 34 | // fmt.Println(h.LinkFlags) 35 | 36 | // lt, err := lnk.LinkTarget(fi) 37 | // if err != nil { 38 | // panic(err) 39 | // } 40 | // _ = lt 41 | // fmt.Println(lnk.StructToJSON(lt, true)) 42 | 43 | // li, err := lnk.LinkInfo(fi) 44 | // if err != nil { 45 | // panic(err) 46 | // } 47 | // _ = li 48 | // fmt.Println(lnk.StructToJSON(li, true)) 49 | 50 | // st, err := lnk.StringData(fi, h.LinkFlags) 51 | // if err != nil { 52 | // panic(err) 53 | // } 54 | // _ = st 55 | // fmt.Println(lnk.StructToJSON(st, true)) 56 | 57 | // edb, err := lnk.DataBlock(fi) 58 | // if err != nil { 59 | // panic(err) 60 | // } 61 | // _ = edb 62 | // // fmt.Println(lnk.StructToJSON(edb, true)) 63 | 64 | ln, err := lnk.Read(fi) 65 | if err != nil { 66 | panic(err) 67 | } 68 | fmt.Println(ln.Header) 69 | 70 | } 71 | -------------------------------------------------------------------------------- /test/nem.test: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parsiya/golnk/740a4c27c4ff809937af0497da020565a3e12113/test/nem.test -------------------------------------------------------------------------------- /test/parseStartMenu.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/parsiya/golnk" 9 | ) 10 | 11 | // Sample program to parse all lnk files in the "All Users" start menu at 12 | // C:\ProgramData\Microsoft\Windows\Start Menu\Programs. 13 | 14 | func main() { 15 | startMenu := "C:/ProgramData/Microsoft/Windows/Start Menu/Programs" 16 | 17 | basePaths := []string{} 18 | 19 | err := filepath.Walk(startMenu, func(path string, info os.FileInfo, walkErr error) error { 20 | // Only look for lnk files. 21 | if filepath.Ext(info.Name()) == ".lnk" { 22 | f, lnkErr := lnk.File(path) 23 | // Print errors and move on to the next file. 24 | if lnkErr != nil { 25 | fmt.Println(lnkErr) 26 | return nil 27 | } 28 | var targetPath = "" 29 | if f.LinkInfo.LocalBasePath != "" { 30 | targetPath = f.LinkInfo.LocalBasePath 31 | } 32 | if f.LinkInfo.LocalBasePathUnicode != "" { 33 | targetPath = f.LinkInfo.LocalBasePathUnicode 34 | } 35 | if targetPath != "" { 36 | fmt.Println("Found", targetPath) 37 | basePaths = append(basePaths, targetPath) 38 | } 39 | } 40 | return nil 41 | }) 42 | if err != nil { 43 | panic(err) 44 | } 45 | 46 | // Print everything. 47 | fmt.Println("------------------------") 48 | for _, p := range basePaths { 49 | fmt.Println(p) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/remote.directory.xp.test: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parsiya/golnk/740a4c27c4ff809937af0497da020565a3e12113/test/remote.directory.xp.test -------------------------------------------------------------------------------- /test/remote.file.xp.test: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parsiya/golnk/740a4c27c4ff809937af0497da020565a3e12113/test/remote.file.xp.test -------------------------------------------------------------------------------- /test/test-orig.lnk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parsiya/golnk/740a4c27c4ff809937af0497da020565a3e12113/test/test-orig.lnk -------------------------------------------------------------------------------- /test/test.lnk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parsiya/golnk/740a4c27c4ff809937af0497da020565a3e12113/test/test.lnk -------------------------------------------------------------------------------- /test/test.lnk.bak: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parsiya/golnk/740a4c27c4ff809937af0497da020565a3e12113/test/test.lnk.bak -------------------------------------------------------------------------------- /test/vbox-svr-win10.lnk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parsiya/golnk/740a4c27c4ff809937af0497da020565a3e12113/test/vbox-svr-win10.lnk -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package lnk 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | // Utilities. 8 | 9 | // StructToJSON converts a struct into the equivalent JSON string. 10 | // String is indented if indent is true. 11 | // Remember only exported fields can be seen by the json package. 12 | func StructToJSON(v interface{}, indent bool) string { 13 | // TODO: Should we panic instead? 14 | js, _ := json.MarshalIndent(v, "", " ") 15 | return string(js) 16 | } 17 | 18 | // reverse returns its argument string reversed rune-wise left to right. 19 | // Taken from https://github.com/golang/example/blob/master/stringutil/reverse.go. 20 | func reverse(s string) string { 21 | r := []rune(s) 22 | for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 { 23 | r[i], r[j] = r[j], r[i] 24 | } 25 | return string(r) 26 | } 27 | --------------------------------------------------------------------------------