File Coverage

blib/lib/Parse/Lnk.pm
Criterion Covered Total %
statement 293 321 91.2
branch 62 82 75.6
condition 10 15 66.6
subroutine 20 20 100.0
pod 5 5 100.0
total 390 443 88.0


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