File Coverage

blib/lib/Parse/Lnk.pm
Criterion Covered Total %
statement 290 318 91.1
branch 62 82 75.6
condition 10 15 66.6
subroutine 19 19 100.0
pod 5 5 100.0
total 386 439 87.9


line stmt bran cond sub pod time code
1             package Parse::Lnk;
2            
3             # Based on the contents of the document:
4             # http://www.i2s-lab.com/Papers/The_Windows_Shortcut_File_Format.pdf
5            
6 2     2   266549 use 5.006;
  2         8  
7 2     2   13 use strict;
  2         13  
  2         96  
8 2     2   11 use warnings;
  2         11  
  2         144  
9 2     2   14 use Carp qw(croak);
  2         17  
  2         8586  
10            
11             our $VERSION = '0.10';
12            
13             =pod
14            
15             =encoding latin1
16            
17             =head1 NAME
18            
19             Parse::Lnk - A cross-platform, depencency free, Windows shortcut (.lnk) meta data parser.
20            
21             =head1 VERSION
22            
23             Version 0.10
24            
25             =cut
26            
27             require Exporter;
28             our @ISA = qw(Exporter);
29             our @EXPORT_OK = qw(parse_lnk resolve_lnk);
30            
31             our $map = { # Tag names made up based on the docs
32             flag => {
33             0 => {
34             0 => 'NO SHELLIDLIST',
35             1 => 'HAS SHELLIDLIST',
36             },
37             1 => {
38             0 => 'NOT POINT TO FILE/DIR',
39             1 => 'POINTS TO FILE/DIR',
40             },
41             2 => {
42             0 => 'NO DESCRIPTION',
43             1 => 'HAS DESCRIPTION',
44             },
45             3 => {
46             0 => 'NO RELATIVE PATH STRING',
47             1 => 'HAS RELATIVE PATH STRING',
48             },
49             4 => {
50             0 => 'NO WORKING DIRECTORY',
51             1 => 'HAS WORKING DIRECTORY',
52             },
53             5 => {
54             0 => 'NO CMD LINE ARGS',
55             1 => 'HAS CMD LINE ARGS',
56             },
57             6 => {
58             0 => 'NO CUSTOM ICON',
59             1 => 'HAS CUSTOM ICON',
60             },
61             },
62             file => {
63             0 => 'READ ONLY TARGET',
64             1 => 'HIDDEN TARGET',
65             2 => 'SYSTEM FILE TARGET',
66             3 => 'VOLUME LABEL TARGET (not possible)',
67             4 => 'DIRECTORY TARGET',
68             5 => 'ARCHIVE',
69             6 => 'NTFS EFS',
70             7 => 'NORMAL TARGET',
71             8 => 'TEMP. TARGET',
72             9 => 'SPARSE TARGET',
73             10 => 'REPARSE POINT DATA TARGET',
74             11 => 'COMPRESSED TARGET',
75             12 => 'TARGET OFFLINE',
76             },
77             show_wnd => {
78             0 => 'SW_HIDE',
79             1 => 'SW_NORMAL',
80             2 => 'SW_SHOWMINIMIZED',
81             3 => 'SW_SHOWMAXIMIZED',
82             4 => 'SW_SHOWNOACTIVE',
83             5 => 'SW_SHOW',
84             6 => 'SW_MINIMIZE',
85             7 => 'SW_SHOWMINNOACTIVE',
86             8 => 'SW_SHOWNA',
87             9 => 'SW_RESTORE',
88             10 => 'SW_SHOWDEFAULT',
89             },
90             vol_type => {
91             0 => 'Unknown',
92             1 => 'No root directory',
93             2 => 'Removable (Floppy, Zip, USB, etc.)',
94             3 => 'Fixed (Hard Disk)',
95             4 => 'Remote (Network Drive)',
96             5 => 'CD-ROM',
97             6 => 'RAM Drive',
98             },
99             };
100            
101             sub resolve_lnk {
102 10     10 1 11414 my $filename = shift;
103 10         125 my $l = __PACKAGE__->new (
104             filename => $filename,
105             resolve => 1,
106             );
107 10         35 $l->_parse;
108 10         58 $l->{base_path};
109             }
110            
111             sub parse_lnk {
112 10     10 1 11596 my $filename = shift;
113 10         50 my $l = __PACKAGE__->new (
114             filename => $filename,
115             );
116 10         35 $l->_parse;
117 10 100       47 return if $l->{error};
118 5         15 $l;
119             }
120            
121             sub new {
122 40     40 1 9159 my $class = shift;
123 40 50 33     269 if (@_ and @_ % 2) {
124 0         0 croak "This method expects (name => value) arguments. Odd number of arguments received";
125             }
126 40         154 my $self = {
127             @_,
128             };
129 40         185 bless $self, $class;
130             }
131            
132             sub from {
133 10     10 1 223783 my $self = shift;
134 10         31 my $filename = shift;
135 10 50       46 $self = $self->new (
136             filename => $filename,
137             ) unless ref $self;
138 10         37 $self->_parse;
139 10 100       72 return if $self->{error};
140 5         20 $self;
141             }
142            
143             sub parse {
144 10     10 1 51 my $self = shift;
145 10         19 my $filename = shift;
146 10 50       54 $self = $self->new (
147             filename => $filename,
148             ) unless ref $self;
149 10         36 $self->_parse;
150 10 100       36 return if $self->{error};
151 5         15 $self;
152             }
153            
154             sub _reset {
155 40     40   65 my $self = shift;
156 40 50       100 return unless ref $self;
157 40         136 for my $k (keys %$self) {
158 50         118 delete $self->{$k};
159             }
160 40         117 $self;
161             }
162            
163             sub _parse {
164 40     40   68 my $self = shift;
165 40         89 my $filename = $self->{filename};
166 40         73 my $resolve = $self->{resolve};
167 40         104 $self->_reset;
168 40 50       105 if (not defined $filename) {
169 0         0 $self->{error} = 'A filename is required';
170 0         0 return;
171             }
172 40 100       1336 if (not -f $filename) {
173 8         30 $self->{error} = "Not a file";
174 8         17 return;
175             }
176 32 50       1632 if (open my $in, '<', $filename) {
177 32         98 binmode $in;
178 32         95 $self->{_fh} = $in;
179             } else {
180             # We set error before croak, in case this call is being eval'ed
181 0         0 $self->{error} = "Can't open file '$filename' for reading";
182 0         0 croak $self->{error};
183             }
184            
185 32         111 my $header = $self->_read_unpack(0, 1);
186 32 100       107 if ($header ne '4c') {
187 12         28 $self->{error} = 'Invalid Lnk file header';
188 12         171 close $self->{_fh};
189 12         26 delete $self->{_fh};
190 12         62 return;
191             }
192            
193 20         51 $self->{guid} = $self->_read_unpack(4, 16);
194            
195 20         80 my $flags = $self->_read_unpack_bin(20, 1);
196 20         35 my $flag_cnt = 0;
197 20         59 my @flag_bits = (0, 0, 0, 0, 0, 0, 0, 0);
198 20         69 while ($flag_cnt < 7) {
199 140         235 my $flag_bit = substr $flags, $flag_cnt, 1;
200 140         197 push @{$self->{flags}}, $map->{flag}->{$flag_cnt}->{$flag_bit};
  140         456  
201 140 100       351 if ($flag_bit eq '1') {
202 40 50 33     178 if ($flag_cnt >= 0 and $flag_cnt <= 6) {
203 40         76 $flag_bits[$flag_cnt] = 1;
204             }
205             }
206 140         274 $flag_cnt++;
207             }
208            
209             # File Attributes 4bytes@18h = 24d
210             # Only a non-zero if "Flag bit 1" above is set to 1
211             #
212 20 50       45 if ($flag_bits[1] == 1) {
213 20         51 my $file_attrib = $self->_read_unpack_bin(24, 2);
214 20         29 my $file_att_cnt = 0;
215 20         69 while ($file_att_cnt < 13) {
216 260         579 my $file_bit = substr $file_attrib, $file_att_cnt, 1;
217 260 100       390 push @{$self->{attributes}}, $map->{file}->{$file_att_cnt} if $file_bit;
  28         134  
218 260         421 $file_att_cnt++;
219             }
220             }
221            
222             # Create time 8bytes @ 1ch = 28
223 20         50 my $ctime = $self->_read_unpack(28, 8);
224 20         119 $ctime = Parse::Windows::Shortcut::Bigint::bighex($self->_reverse_hex($ctime));
225 20         1048 $ctime = $self->_MStime_to_unix($ctime);
226 20         80 $self->{create_time} = $ctime;
227            
228             # Access time 8 bytes@ 0x24 = 36D
229 20         64 my $atime = $self->_read_unpack(36, 8);
230 20         48 $atime = Parse::Windows::Shortcut::Bigint::bighex($self->_reverse_hex($atime));
231 20         908 $atime = $self->_MStime_to_unix($atime);
232 20         54 $self->{last_accessed_time} = $atime;
233            
234             # Mod Time8b @ 0x2C = 44D
235 20         47 my $mtime = $self->_read_unpack(44, 8);
236 20         40 $mtime = Parse::Windows::Shortcut::Bigint::bighex($self->_reverse_hex($mtime));
237 20         789 $mtime = $self->_MStime_to_unix($mtime);
238 20         48 $self->{modified_time} = $mtime;
239            
240             # Target File length starts @ 34h = 52d
241 20         44 my $f_len = $self->_read_unpack(52, 4);
242 20         40 $f_len = hex $self->_reverse_hex($f_len);
243 20         67 $self->{target_length} = $f_len;
244            
245             # Icon File info starts @ 38h = 56d
246 20         40 my $ico_num = $self->_read_unpack(56, 4);
247 20         29 $ico_num = hex $ico_num;
248 20         38 $self->{icon_index} = $ico_num;
249            
250             # ShowWnd val to pass to target
251             # Starts @3Ch = 60d
252 20         36 my $show_wnd = $self->_read_unpack(60, 1);
253 20         33 $show_wnd = hex $show_wnd;
254 20         37 $self->{show_wnd} = $show_wnd;
255 20         79 $self->{show_wnd_flag} = $map->{show_wnd}->{$show_wnd};
256            
257             # Hot key
258             # Starts @40h = 64d
259 20         39 my $hot_key = $self->_read_unpack(64, 4);
260 20         31 $hot_key = hex $hot_key;
261 20         41 $self->{hot_key} = $hot_key;
262            
263             # ItemID List
264             # Read size of item ID list
265 20         32 my $i_len = $self->_read_unpack(76, 2);
266 20         35 $i_len = hex $self->_reverse_hex($i_len);
267             # skip to end of list
268 20         31 my $end_of_list = (78 + $i_len);
269            
270             # FileInfo structure
271             #
272 20         23 my $struc_start = $end_of_list;
273 20         23 my $first_off_off = ($struc_start + 4);
274 20         22 my $vol_flags_off = ($struc_start + 8);
275 20         47 my $local_vol_off = ($struc_start + 12);
276 20         30 my $base_path_off = ($struc_start + 16);
277 20         26 my $net_vol_off = ($struc_start + 20);
278 20         28 my $rem_path_off = ($struc_start + 24);
279            
280             # Structure length
281 20         32 my $struc_len = $self->_read_unpack($struc_start, 4);
282 20         34 $struc_len = hex $self->_reverse_hex($struc_len);
283 20         29 my $struc_end = $struc_start + $struc_len;
284            
285             # First offset after struct - Should be 1C under normal circumstances
286 20         36 my $first_off = $self->_read_unpack($first_off_off, 1);
287            
288             # File location flags
289 20         42 my $vol_flags = $self->_read_unpack_bin($vol_flags_off, 1);
290 20         40 $vol_flags = substr $vol_flags, 0, 2;
291 20         41 my @vol_bits = (0, 0);
292 20 100       71 if ($vol_flags =~ /10/) {
293 8         19 $self->{target_type} = 'local';
294 8         14 $vol_bits[0] = 1;
295 8         15 $vol_bits[1] = 0;
296             }
297             # Haven't found this case yet...
298 20 100       47 if ($vol_flags =~ /01/) {
299 4         6 $self->{target_type} = 'network';
300 4         5 $vol_bits[0] = 0;
301 4         5 $vol_bits[1] = 1;
302             }
303             # But this one I did:
304 20 100       51 if ($vol_flags =~ /11/) {
305 8         14 $self->{target_type} = 'network';
306 8         11 $vol_bits[0] = 1;
307 8         11 $vol_bits[1] = 1;
308             }
309            
310             # Local volume table
311             # Random garbage if bit0 is clear in volume flags
312 20 100 100     94 if ($vol_bits[0] == 1 and $vol_bits[1] == 0) {
313             # This is the offset of the local volume table within the
314             #File Info Location Structure
315 8         23 my $loc_vol_tab_off = $self->_read_unpack($local_vol_off, 4);
316 8         18 $loc_vol_tab_off = hex $self->_reverse_hex($loc_vol_tab_off);
317            
318             # This is the asolute start location of the local volume table
319 8         16 my $loc_vol_tab_start = $loc_vol_tab_off + $struc_start;
320            
321             # This is the length of the local volume table
322 8         47 my $local_vol_len = $self->_read_unpack(($loc_vol_tab_off + $struc_start), 4);
323 8         21 $local_vol_len = hex $self->_reverse_hex($local_vol_len);
324            
325             # We now have enough info to
326             # Calculate the end of the local volume table.
327 8         16 my $local_vol_tab_end = $loc_vol_tab_start + $local_vol_len;
328            
329             # This is the volume type
330 8         12 my $curr_tab_offset = $loc_vol_tab_off + $struc_start + 4;
331 8         44 my $vol_type = $self->_read_unpack($curr_tab_offset, 4);
332 8         21 $vol_type = hex $self->_reverse_hex($vol_type);
333 8         33 $self->{volume_type} = $map->{vol_type}->{$vol_type};
334            
335             # Volume Serial Number
336 8         14 $curr_tab_offset = $loc_vol_tab_off + $struc_start + 8;
337 8         20 my $vol_serial = $self->_read_unpack($curr_tab_offset, 4);
338 8         34 $vol_serial = $self->_reverse_hex($vol_serial);
339 8         22 $self->{volume_serial} = $vol_serial;
340            
341             # Get the location, and length of the volume label
342             # we should really read the vol_label_loc from offset Ch
343 8         12 my $vol_label_loc = $loc_vol_tab_off + $struc_start + 16;
344 8         14 my $vol_label_len = $local_vol_tab_end - $vol_label_loc;
345 8         19 my $vol_label = $self->_read_unpack_ascii($vol_label_loc, $vol_label_len);
346 8         19 $self->{volume_label} = $vol_label;
347            
348             # This is the offset of the base path info within the
349             # File Info structure
350             # Random Garbage when bit0 is clear in volume flags
351 8         17 my $base_path_off = $self->_read_unpack($base_path_off, 4);
352 8         17 $base_path_off = hex $self->_reverse_hex($base_path_off);
353 8         14 $base_path_off = $struc_start + $base_path_off;
354            
355             # Read base path data upto NULL term
356 8         19 my $bp_data = $self->_read_null_term($base_path_off);
357 8         22 $self->{base_path} = $bp_data;
358 8 100       22 if ($resolve) {
359 2         33 close $self->{_fh};
360 2         6 delete $self->{_fh};
361 2         14 return $self;
362             }
363             }
364            
365             # Network Volume Table
366 18 100 66     63 if ($vol_bits[0] == 0 and $vol_bits[1] == 1) {
367 4         10 $net_vol_off = hex $self->_reverse_hex($self->_read_unpack($net_vol_off, 4));
368 4         7 $net_vol_off = $struc_start + $net_vol_off;
369 4         6 my $net_vol_len = $self->_read_unpack($net_vol_off, 4);
370 4         5 $net_vol_len = hex $self->_reverse_hex($net_vol_len);
371            
372             # Network Share Name
373 4         6 my $net_share_name_off = $net_vol_off + 8;
374 4         7 my $net_share_name_loc = hex $self->_reverse_hex($self->_read_unpack($net_share_name_off, 4));
375 4 50       10 if ($net_share_name_loc ne "20") {
376 0         0 close delete $self->{_fh};
377 0         0 $self->{error} = 'Error: NSN ofset should always be 14h';
378 0         0 close $self->{_fh};
379 0         0 delete $self->{_fh};
380 0         0 return $self;
381             }
382 4         5 $net_share_name_loc = $net_vol_off + $net_share_name_loc;
383 4         7 my $net_share_name = $self->_read_null_term($net_share_name_loc);
384 4         8 $self->{base_path} = $net_share_name;
385 4 100       7 if ($resolve) {
386 1         14 close $self->{_fh};
387 1         3 delete $self->{_fh};
388 1         6 return $self;
389             }
390            
391             # Mapped Network Drive Info
392 3         4 my $net_share_mdrive = $net_vol_off + 12;
393 3         4 $net_share_mdrive = $self->_read_unpack($net_share_mdrive, 4);
394 3         39 $net_share_mdrive = hex $self->_reverse_hex($net_share_mdrive);
395 3 50       7 if ($net_share_mdrive ne "0") {
396 3         4 $net_share_mdrive = $net_vol_off + $net_share_mdrive;
397 3         5 $net_share_mdrive = $self->_read_null_term($net_share_mdrive);
398 3         7 $self->{mapped_drive} = $net_share_mdrive;
399             }
400             }
401            
402 17 100 100     66 if ($vol_bits[0] == 1 and $vol_bits[1] == 1) {
403             # Finding the location, as I'm not sure this is always 104
404 8         13 for my $i (1..10000) {
405 208         212 my $n = 4 * $i;
406 208         289 my $l = $self->_read_unpack($n, 4);
407 208         273 $l = hex $self->_reverse_hex($l);
408 208         229 my $net_share_name_off = $n + 8;
409 208         296 my $net_share_name_loc = hex $self->_reverse_hex($self->_read_unpack($net_share_name_off, 4));
410 208 100       375 if ($net_share_name_loc ne "20") {
411 200         251 next;
412             }
413 8         9 $net_vol_off = $n;
414 8         10 last;
415             }
416            
417 8         13 my $net_vol_len = $self->_read_unpack($net_vol_off, 4);
418 8         15 $net_vol_len = hex $self->_reverse_hex($net_vol_len);
419            
420             # Network Share Name
421 8         10 my $net_share_name_off = $net_vol_off + 8;
422 8         16 my $net_share_name_loc = hex $self->_reverse_hex($self->_read_unpack($net_share_name_off, 4));
423 8 50       18 if ($net_share_name_loc ne "20") {
424 0         0 close delete $self->{_fh};
425 0         0 $self->{error} = 'Error: NSN ofset should always be 14h';
426 0         0 close $self->{_fh};
427 0         0 delete $self->{_fh};
428 0         0 return $self;
429             }
430 8         8 $net_share_name_loc = $net_vol_off + $net_share_name_loc;
431 8         16 my $net_share_name = $self->_read_null_term($net_share_name_loc);
432 8         16 $self->{base_path} = $net_share_name;
433 8 100       14 if ($resolve) {
434 2         28 close $self->{_fh};
435 2         5 delete $self->{_fh};
436 2         10 return $self;
437             }
438            
439             # Mapped Network Drive Info
440 6         6 my $net_share_mdrive = $net_vol_off + 12;
441 6         9 $net_share_mdrive = $self->_read_unpack($net_share_mdrive, 4);
442 6         7 $net_share_mdrive = hex $self->_reverse_hex($net_share_mdrive);
443 6 50       13 if ($net_share_mdrive ne "0") {
444 0         0 $net_share_mdrive = $net_vol_off + $net_share_mdrive;
445 0         0 $net_share_mdrive = $self->_read_null_term($net_share_mdrive);
446 0         0 $self->{mapped_drive} = $net_share_mdrive;
447             }
448             }
449            
450             #Remaining Path
451 15         25 $rem_path_off = $self->_read_unpack($rem_path_off, 4);
452 15         25 $rem_path_off = hex $self->_reverse_hex($rem_path_off);
453 15         22 $rem_path_off = $struc_start + $rem_path_off;
454 15         23 my $rem_data = $self->_read_null_term($rem_path_off);
455 15         28 $self->{remaining_path} = $rem_data;
456            
457             # The next starting location is the end of the structure
458 15         18 my $next_loc = $struc_end;
459 15         20 my $addnl_text;
460            
461             # Description String
462             # present if bit2 is set in header flags.
463 15 50       53 if ($flag_bits[2] eq "1") {
464 0         0 ($addnl_text, $next_loc) = $self->_add_info($next_loc);
465 0         0 $self->{description} = $addnl_text;
466 0         0 $next_loc = $next_loc + 1;
467             }
468            
469             # Relative Path
470 15 50       28 if ($flag_bits[3] eq "1") {
471 0         0 ($addnl_text, $next_loc) = $self->_add_info($next_loc);
472 0         0 $self->{relative_path} = $addnl_text;
473 0         0 $next_loc = $next_loc + 1;
474             }
475             # Working Dir
476 15 100       28 if ($flag_bits[4] eq "1") {
477 6         32 ($addnl_text, $next_loc) = $self->_add_info($next_loc);
478 6         43 ($self->{working_directory} = $addnl_text) =~ s/\x00//g;
479 6         13 $next_loc = $next_loc + 1;
480             }
481             # CMD Line
482 15 50       33 if ($flag_bits[5] eq "1") {
483 0         0 ($addnl_text, $next_loc) = $self->_add_info($next_loc);
484 0         0 $self->{command_line} = $addnl_text;
485 0         0 $next_loc = $next_loc + 1;
486             }
487             #Icon filename
488 15         25 ($addnl_text, $next_loc) = $self->_add_info($next_loc);
489 15 50       35 if ($flag_bits[6] eq "1") {
490 0         0 $self->{icon_filename} = $addnl_text;
491             }
492 15         216 close delete $self->{_fh};
493 15         94 $self;
494             }
495            
496             sub _add_info {
497 21     21   27 my $self = shift;
498 21         31 my ($tmp_start_loc) = shift;
499 21         135 my $tmp_len = 2 * hex $self->_reverse_hex($self->_read_unpack($tmp_start_loc, 1));
500 21         29 $tmp_start_loc++;
501 21 100       44 if ($tmp_len ne "0") {
502 15         48 my $tmp_string = $self->_read_unpack_ascii($tmp_start_loc, $tmp_len);
503 15         25 my $now_loc = tell;
504 15         43 return ($tmp_string, $now_loc);
505             } else {
506 6         8 my $now_loc = tell;
507 6         6 my $tmp_string = 'Null';
508 6         14 return ($tmp_string, $now_loc);
509             }
510             }
511            
512             sub _read_unpack {
513 781     781   891 my $self = shift;
514 781         1133 my ($loc, $bites) = @_;
515 781         846 my $tmp_data;
516 781 50       5713 seek ($self->{_fh}, $loc, 0) or croak "Can't seek to $loc";
517 781         4343 read $self->{_fh}, $tmp_data, $bites;
518 781         1577 $tmp_data = unpack 'H*', $tmp_data;
519 781         1582 return $tmp_data;
520             }
521            
522             sub _read_unpack_ascii {
523 23     23   30 my $self = shift;
524 23         195 my ($loc, $bites) = @_;
525 23         31 my $tmp_data;
526 23 50       290 seek ($self->{_fh}, $loc, 0) or croak "Can't seek to $loc\n";
527 23         164 read $self->{_fh}, $tmp_data, $bites;
528 23         94 $tmp_data = unpack 'A*', $tmp_data;
529 23         53 return $tmp_data;
530             }
531            
532             sub _read_unpack_bin {
533 60     60   85 my $self = shift;
534 60         146 my ($loc, $bites) = @_;
535 60         105 my $tmp_data;
536 60 50       560 seek ($self->{_fh}, $loc, 0) or croak "Can't seek to $loc\n";
537 60         357 read $self->{_fh}, $tmp_data, $bites;
538 60         153 $tmp_data = unpack 'b*', $tmp_data;
539 60         140 return $tmp_data;
540             }
541            
542             sub _MStime_to_unix {
543 60     60   96 my $self = shift;
544 60         167 my $mstime_dec = shift;
545             # The number of seconds between Unix/FILETIME epochs
546 60         74 my $MSConversion = '11644473600';
547             # Convert 100ms increments to Seconds.
548 60         240 $mstime_dec *= .0000001;
549             # Add difference in epochs
550 60         122 $mstime_dec -= $MSConversion;
551 60         554 sprintf '%0.3f', $mstime_dec;
552             }
553            
554             sub _reverse_hex {
555 649     649   806 my $self = shift;
556 649         690 my $HEXDATE = shift;
557 649         637 my @bytearry;
558 649         647 my $byte_cnt = 0;
559 649 100       1120 my $max_byte_cnt = length($HEXDATE) < 16 ? int(length($HEXDATE) / 2) : 8;
560 649         731 my $byte_offset = 0;
561 649         912 while ($byte_cnt < $max_byte_cnt) {
562 2727         2950 my $tmp_str = substr $HEXDATE, $byte_offset, 2;
563 2727         3216 push @bytearry, $tmp_str;
564 2727         2535 $byte_cnt++;
565 2727         3514 $byte_offset += 2;
566             }
567 649         1574 return join '', reverse @bytearry;
568             }
569            
570             sub _read_null_term {
571 38     38   44 my $self = shift;
572 38         46 my $loc = shift;
573             # Save old record seperator
574 38         70 my $old_rs = $/;
575             # Set new seperator to NULL term.
576 38         73 $/ = "\0";
577 38 50       301 seek ($self->{_fh}, $loc, 0) or die "Can't seek to $loc\n";
578 38         46 my $fh = $self->{_fh};
579 38         204 my $term_data = <$fh>;
580 38 100       73 chomp $term_data if $term_data;
581             # Reset
582 38         61 $/ = $old_rs;
583 38         73 return $term_data;
584             }
585            
586             {
587             package Parse::Windows::Shortcut::Bigint;
588             require Math::BigInt;
589             require bigint;
590            
591             sub bighex {
592 60     60   81 my $v = shift;
593 60         2075 my $h = bigint::hex $v;
594 60         72717 $h.'';
595             }
596             }
597            
598            
599             =head1 SYNOPSIS
600            
601             This module reads Win32 shortcuts (*.lnk files) to obtain the meta data in them.
602            
603             Its goal is to be able to resolve the path they point to (along with other data),
604             from any platform/OS, without the need for extra dependencies.
605            
606             Some examples of usage:
607            
608             use Parse::Lnk;
609            
610             my $data = Parse::Lnk->from($filename);
611            
612             # $data is now a hashref if the file was parsed successfully.
613             # undef if not.
614            
615             ##########
616             # Or ... #
617             ##########
618            
619             use Parse::Lnk qw(parse_lnk);
620            
621             my $data = parse_lnk $filename;
622            
623             # $data is now a hashref if the file was parsed successfully.
624             # undef if not.
625            
626             ##########
627             # Or ... #
628             ##########
629            
630             use Parse::Lnk qw(resolve_lnk);
631            
632             my $path = resolve_lnk $filename;
633            
634             # $path is now a string with the path the lnk file points to.
635             # undef if the lnk file was not parsed successfully.
636            
637             ###############################################################
638             # Or, if you want a little more information/control on errors #
639             ###############################################################
640            
641             use Parse::Lnk;
642            
643             my $lnk = Parse::Lnk->new;
644            
645             $lnk->parse($filename) or die $lnk->{error};
646            
647             # Or:
648            
649             $lnk->parse($filename);
650            
651             if ($lnk->{error}) {
652             # ... do your own error handling;
653             }
654            
655            
656            
657            
658             =head1 EXPORT
659            
660             Nothing is exported by default. You can explicitly import this functions:
661            
662             =head2 parse_lnk($filename)
663            
664             This will return a Parse::Lnk instance, which is a hashref. The keys in that
665             hashref depend on the data that was parsed from the .lnk file.
666            
667             It will return C on error.
668            
669             use Parse::Lnk qw(parse_lnk);
670            
671             my $lnk = parse_lnk $filename;
672            
673             if ($lnk) {
674             print "$filename points to path $lnk->{base_path}\n";
675            
676             my $create_date = localtime $lnk->{create_time};
677             print "$filename was created on $create_date";
678             } else {
679             print "Could not parse $filename";
680             }
681            
682             =head2 resolve_lnk($filename)
683            
684             This will return the path the .lnk file is pointing to.
685            
686             It will return C on error.
687            
688             use Parse::Lnk qw(resolve_lnk);
689            
690             my $path = resolve_lnk $filename;
691            
692             if ($path) {
693             print "$filename points to path $path";
694             } else {
695             print "Could not parse $filename";
696             }
697            
698            
699             =head1 METHODS
700            
701             You can create a C instance and call a few methods on it. This
702             may give you more control/information when something goes wrong while parsing
703             the file.
704            
705             =head2 new
706            
707             This creates a new instance. You can pass the C value as argument,
708             or you can set/change it later.
709            
710             use Parse::Lnk;
711            
712             my $lnk = Parse::Lnk->new(filename => $filename);
713            
714             # or
715            
716             my $lnk = Parse::Lnk->new;
717             $lnk->{filename} = $filename;
718            
719             =head2 parse
720            
721             This method will parse the current C in the instance. You can change
722             the value of C and parse again at any point.
723            
724             use Parse::Lnk;
725            
726             my $lnk = Parse::Lnk->new(filename => $filename);
727            
728             $lnk->parse;
729            
730             if ($lnk->{error}) {
731             # handle the error
732             } else {
733             print "$filename points to $lnk->{base_path}";
734             }
735            
736             for my $other_filename (@filenames) {
737             $lnk->{filename} = $other_filename;
738             $lnk->parse;
739            
740             if ($lnk->{error}) {
741             # handle the error
742             next;
743             }
744            
745             print "$other_filename points to $lnk->{base_path}";
746             }
747            
748             =head2 from
749            
750             It will return a C instance, or undef on error. This method was
751             written with plain package name calling in mind:
752            
753             use Parse::Lnk;
754            
755             my $lnk = Parse::Lnk->from($filename);
756            
757             if ($lnk) {
758             print "$filename points to path $lnk->{base_path}\n";
759            
760             my $create_date = localtime $lnk->{create_time};
761             print "$filename was created on $create_date";
762             } else {
763             print "Could not parse $filename";
764             }
765            
766            
767             =head1 AUTHOR
768            
769             Francisco Zarabozo, C<< >>
770            
771             =head1 BUGS
772            
773             I'm sure there are many. I haven't found bugs with the lnk files I've tested
774             it. If you find a bug or you have a problem reading a shortcut/lnk file,
775             please don't hesitate to report it and don't forget to include the file in
776             question. If you are on Windows, you will have to zip the file in a way that
777             is the lnk file the one being zipped and not the actual directory/file it
778             is pointing to. I promise to look at any report and work on a solution as
779             fast as I can.
780            
781             Please report any bugs or feature requests to C, or through
782             the web interface at L. I will be notified, and then you'll
783             automatically be notified of progress on your bug as I make changes.
784            
785            
786             =head1 SUPPORT
787            
788             You can find documentation for this module with the perldoc command.
789            
790             perldoc Parse::Lnk
791            
792            
793             You can also look for information at:
794            
795             =over 4
796            
797             =item * RT: CPAN's request tracker (report bugs here)
798            
799             L
800            
801             =item * AnnoCPAN: Annotated CPAN documentation
802            
803             L
804            
805             =item * CPAN Ratings
806            
807             L
808            
809             =item * Search CPAN
810            
811             L
812            
813             =back
814            
815            
816             =head1 ACKNOWLEDGEMENTS
817            
818             Many sections of the code were adapted from Jacob Cunningham's
819             L,
820             licensed under the GNU General Public License Version 2.
821            
822            
823             =head1 LICENSE AND COPYRIGHT
824            
825             This software is copyright (c) 2021-2024 by Francisco Zarabozo.
826            
827             This is free software; you can redistribute it and/or modify it under
828             the same terms as the Perl 5 programming language system itself.
829            
830            
831             =cut
832            
833             1;