File Coverage

blib/lib/File/Util/Symlink.pm
Criterion Covered Total %
statement 38 123 30.8
branch 9 62 14.5
condition 0 10 0.0
subroutine 9 10 90.0
pod 4 4 100.0
total 60 209 28.7


line stmt bran cond sub pod time code
1             package File::Util::Symlink;
2              
3 2     2   585129 use 5.010001;
  2         10  
4 2     2   13 use strict;
  2         5  
  2         104  
5 2     2   13 use warnings;
  2         5  
  2         150  
6 2     2   4882 use Log::ger;
  2         136  
  2         12  
7              
8 2     2   679 use Exporter 'import';
  2         7  
  2         101  
9 2     2   13 use File::Spec;
  2         2  
  2         4405  
10              
11             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
12             our $DATE = '2023-11-30'; # DATE
13             our $DIST = 'File-Util-Symlink'; # DIST
14             our $VERSION = '0.006'; # VERSION
15              
16             our @EXPORT_OK = qw(
17             symlink_rel
18             symlink_abs
19             adjust_rel_symlink
20             check_symlink
21             );
22              
23             our %SPEC;
24              
25             sub symlink_rel {
26 2     2 1 551421 my ($dest_path, $link_path) = @_;
27 2         540 symlink(File::Spec->abs2rel($dest_path), $link_path);
28             }
29              
30             sub symlink_abs {
31 2     2 1 6828 my ($dest_path, $link_path) = @_;
32 2         390 symlink(File::Spec->rel2abs($dest_path), $link_path);
33             }
34              
35             sub adjust_rel_symlink {
36 1     1 1 5852 require File::Basename;
37 1         732 require Path::Naive;
38              
39 1         1566 my ($link_path1, $link_path2) = @_;
40              
41 1 50       27 unless (-l $link_path1) {
42 0         0 log_warn "First path '$link_path1' is not a symlink, skipping adjusting";
43 0         0 return;
44             }
45 1 50       14 unless (-l $link_path2) {
46 0         0 log_warn "Second path '$link_path2' is not a symlink, skipping adjusting";
47 0         0 return;
48             }
49              
50 1         47 my $dest_path1 = readlink $link_path1;
51 1 50       6 if (!defined $dest_path1) {
52 0         0 log_warn "Cannot read first symlink %s, skipping adjusting", $link_path1;
53 0         0 return;
54             }
55 1         36 my $dest_path2 = readlink $link_path2;
56 1 50       5 if (!defined $dest_path2) {
57 0         0 log_warn "Cannot read second symlink %s, skipping adjusting", $link_path2;
58 0         0 return;
59             }
60              
61 1 50       15 if (File::Spec->file_name_is_absolute($dest_path1)) {
62 0         0 log_trace "First symlink %s (target '%s') is not relative path, skipping adjusting", $link_path1, $dest_path1;
63 0         0 return;
64             }
65 1 50       7 if (File::Spec->file_name_is_absolute($dest_path2)) {
66 0         0 log_trace "Second symlink %s (target '%s') is not relative path, skipping adjusting", $link_path2, $dest_path2;
67 0         0 return;
68             }
69 1         271 my $new_dest_path2 = Path::Naive::normalize_path(
70             File::Spec->abs2rel(
71             (File::Spec->rel2abs($dest_path1, File::Basename::dirname($link_path1))),
72             File::Spec->rel2abs(File::Basename::dirname(File::Spec->rel2abs($link_path2)), "/"), # XXX "/" is unixism
73             )
74             );
75 1 50       74 if ($dest_path2 eq $new_dest_path2) {
76 0         0 log_trace "Skipping adjusting second symlink %s (no change: %s)", $link_path2, $new_dest_path2;
77 0         0 return;
78             }
79 1 50       157 unlink $link_path2 or do {
80 0         0 log_error "Cannot adjust second symlink %s (can't unlink: %s)", $link_path2, $!;
81 0         0 return;
82             };
83 1 50       170 symlink($new_dest_path2, $link_path2) or do {
84 0         0 log_error "Cannot adjust second symlink %s (can't symlink to '%s': %s)", $link_path2, $new_dest_path2, $!;
85 0         0 return;
86             };
87 1         9 log_trace "Adjusted symlink %s (from target '%s' to target '%s')", $link_path2, $dest_path2, $new_dest_path2;
88 1         6 1;
89             }
90              
91             $SPEC{check_symlink} = {
92             v => 1.1,
93             summary => "Perform various checks on a symlink",
94             args => {
95             symlink => {
96             summary => "Path to the symlink to be checked",
97             schema => "filename*",
98             req => 1,
99             pos => 0,
100             },
101             target => {
102             summary => "Expected target path",
103             schema => "filename*",
104             pos => 1,
105             description => <<'_',
106              
107             If specified, then target of symlink (after normalized to absolute path) will be
108             checked and must point to this target.
109              
110             _
111             },
112             is_abs => {
113             summary => 'Whether we should check that symlink target is an absolute path',
114             schema => 'bool',
115             description => <<'_',
116              
117             If set to true, then symlink target must be an absolute path. If
118             set to false, then symlink target must be a relative path.
119              
120             _
121             cmdline_aliases => {
122             is_rel => {is_flag=>1, summary=>'Alias for --isnt-abs', code=>sub { $_[0]{is_abs} = 0 }},
123             isnt_rel => {is_flag=>1, summary=>'Alias for --is-abs', code=>sub { $_[0]{is_abs} = 1 }},
124             },
125             },
126             ext_matches => {
127             summary => 'Whether extension should match',
128             schema => 'bool',
129             description => <<'_',
130              
131             If set to true, then if both symlink name and target filename contain filename
132             extension (e.g. `jpg`) then they must match. Case variation is allowed (e.g.
133             `JPG`) but other variation is not (e.g. `jpeg`).
134              
135             _
136             },
137             content_matches => {
138             summary => 'Whether content should match extension',
139             schema => 'bool',
140             description => <<'_',
141              
142             If set to true, will guess media type from content and check that file extension
143             exists nd matches the media type. Requires , which is
144             only specified as a "Recommends" dependency by File-Symlink-Util distribution.
145              
146             _
147             },
148             },
149             };
150              
151             sub check_symlink {
152 0     0 1   my %args = @_;
153 0           my $res = [200, "OK", []];
154              
155 0 0         my $symlink; defined($symlink = $args{symlink}) or return [400, "Please specify 'symlink' argument"];
  0            
156 0 0         (-l $symlink) or do { push @{ $res->[2] }, (-e _) ? "File is not a symlink" : "File does not exist"; goto END_CHECK };
  0 0          
  0            
  0            
157 0           my $target = readlink $symlink;
158 0 0         (-e $target) or do { push @{ $res->[2] }, "Broken symlink, target does not exist ($target)"; goto END_CHECK };
  0            
  0            
  0            
159 0 0         if (defined $args{is_abs}) {
160 0           require File::Spec;
161 0 0         if ($args{is_abs}) {
162 0 0         unless (File::Spec->file_name_is_absolute($target)) {
163 0           push @{ $res->[2] }, "Symlink target is not an absolute path";
  0            
164             }
165             } else {
166 0 0         if (File::Spec->file_name_is_absolute($target)) {
167 0           push @{ $res->[2] }, "Symlink target is not a relative path";
  0            
168             }
169             }
170             }
171 0 0         if (defined $args{target}) {
172 0           require Cwd;
173 0           my $wanted_abs_target = Cwd::abs_path($args{target});
174 0           my $abs_target = Cwd::abs_path($target);
175 0 0         unless ($wanted_abs_target eq $abs_target) {
176 0           push @{ $res->[2] }, "Symlink target is not the same as wanted ($args{target})";
  0            
177             }
178             }
179             CHECK_EXT_MATCHES: {
180 0 0         if ($args{ext_matches}) {
  0            
181 0           my ($symlink_ext) = $symlink =~ /\.(\w+)\z/;
182 0           my ($target_ext) = $target =~ /\.(\w+)\z/;
183 0 0 0       last CHECK_EXT_MATCHES unless defined $symlink_ext && defined $target_ext;
184 0 0         unless (lc($symlink_ext) eq lc($target_ext)) {
185 0           push @{ $res->[2] }, "Symlink extension ($symlink_ext) does not match target's ($target_ext)";
  0            
186             }
187             }
188             }
189             CHECK_CONTENT_MATCHES: {
190 0 0         if ($args{content_matches}) {
  0            
191 0           require File::MimeInfo::Magic;
192 0           my ($symlink_ext) = $symlink =~ /\.(\w+)\z/;
193 0 0         open my $fh, "<", $symlink or do { push @{ $res->[2] }, "Can't open symlink target for content checking: $!"; last CHECK_CONTENT_MATCHES };
  0            
  0            
  0            
194 0           my $type = File::MimeInfo::Magic::mimetype($fh);
195 0 0         my @exts; @exts = File::MimeInfo::Magic::extensions($type) if $type;
  0            
196 0 0 0       if (defined($symlink_ext) && @exts) {
    0 0        
197 0           my $found;
198 0           for my $ext (@exts) {
199 0 0         if (lc $ext eq lc $symlink_ext) { $found++; last }
  0            
  0            
200             }
201 0 0         unless ($found) {
202 0           push @{ $res->[2] }, "Symlink extension ($symlink_ext) does not match content type ($type, exts=".join("|", @exts).")";
  0            
203             }
204             } elsif (defined($symlink_ext) xor @exts) {
205 0 0         if (defined $symlink_ext) {
206 0           push @{ $res->[2] }, "Content type is unknown but symlink has extension ($symlink_ext)";
  0            
207             } else {
208 0           push @{ $res->[2] }, "Content type is $type but symlink does not have any extension";
  0            
209             }
210             } else {
211             # mime type is unknown and file does not have extension -> OK
212             }
213             }
214             }
215              
216             END_CHECK:
217 0 0         if (@{ $res->[2] }) { $res->[0] = 500; $res->[1] = "Errors" }
  0            
  0            
  0            
218 0           $res;
219             }
220              
221             1;
222             # ABSTRACT: Utilities related to symbolic links
223              
224             __END__