File Coverage

blib/lib/File/Symlink/Util.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::Symlink::Util;
2              
3 1     1   74132 use 5.010001;
  1         15  
4 1     1   5 use strict;
  1         2  
  1         19  
5 1     1   5 use warnings;
  1         2  
  1         22  
6 1     1   1837 use Log::ger;
  1         50  
  1         4  
7              
8 1     1   285 use Exporter 'import';
  1         2  
  1         28  
9 1     1   5 use File::Spec;
  1         2  
  1         1246  
10              
11             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
12             our $DATE = '2023-06-30'; # DATE
13             our $DIST = 'File-Symlink-Util'; # DIST
14             our $VERSION = '0.003'; # 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 3760 my ($dest_path, $link_path) = @_;
27 2         269 symlink(File::Spec->abs2rel($dest_path), $link_path);
28             }
29              
30             sub symlink_abs {
31 2     2 1 3768 my ($dest_path, $link_path) = @_;
32 2         117 symlink(File::Spec->rel2abs($dest_path), $link_path);
33             }
34              
35             sub adjust_rel_symlink {
36 1     1 1 2945 require File::Basename;
37 1         542 require Path::Naive;
38              
39 1         1156 my ($link_path1, $link_path2) = @_;
40              
41 1 50       17 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       12 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         21 my $dest_path1 = readlink $link_path1;
51 1 50       4 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         15 my $dest_path2 = readlink $link_path2;
56 1 50       4 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       11 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       5 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         206 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       84 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       64 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       38 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         10 log_trace "Adjusted symlink %s (from target '%s' to target '%s')", $link_path2, $dest_path2, $new_dest_path2;
88 1         5 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             },
122             ext_matches => {
123             summary => 'Whether extension should match',
124             schema => 'bool',
125             description => <<'_',
126              
127             If set to true, then if both symlink name and target filename contain filename
128             extension (e.g. `jpg`) then they must match. Case variation is allowed (e.g.
129             `JPG`) but other variation is not (e.g. `jpeg`).
130              
131             _
132             },
133             content_matches => {
134             summary => 'Whether content should match extension',
135             schema => 'bool',
136             description => <<'_',
137              
138             If set to true, will guess media type from content and check that file extension
139             exists nd matches the media type. Requires , which is
140             only specified as a "Recommends" dependency by File-Symlink-Util distribution.
141              
142             _
143             },
144             },
145             };
146              
147             sub check_symlink {
148 0     0 1   my %args = @_;
149 0           my $res = [200, "OK", []];
150              
151 0 0         my $symlink; defined($symlink = $args{symlink}) or return [400, "Please specify 'symlink' argument"];
  0            
152 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            
153 0           my $target = readlink $symlink;
154 0 0         (-e $target) or do { push @{ $res->[2] }, "Broken symlink, target does not exist ($target)"; goto END_CHECK };
  0            
  0            
  0            
155 0 0         if (defined $args{is_abs}) {
156 0           require File::Spec;
157 0 0         if ($args{is_abs}) {
158 0 0         unless (File::Spec->file_name_is_absolute($target)) {
159 0           push @{ $res->[2] }, "Symlink target is not an absolute path";
  0            
160             }
161             } else {
162 0 0         if (File::Spec->file_name_is_absolute($target)) {
163 0           push @{ $res->[2] }, "Symlink target is not a relative path";
  0            
164             }
165             }
166             }
167 0 0         if (defined $args{target}) {
168 0           require Cwd;
169 0           my $wanted_abs_target = Cwd::abs_path($args{target});
170 0           my $abs_target = Cwd::abs_path($target);
171 0 0         unless ($wanted_abs_target eq $abs_target) {
172 0           push @{ $res->[2] }, "Symlink target is not the same as wanted ($args{target})";
  0            
173             }
174             }
175             CHECK_EXT_MATCHES: {
176 0 0         if ($args{ext_matches}) {
  0            
177 0           my ($symlink_ext) = $symlink =~ /\.(\w+)\z/;
178 0           my ($target_ext) = $target =~ /\.(\w+)\z/;
179 0 0 0       last CHECK_EXT_MATCHES unless defined $symlink_ext && defined $target_ext;
180 0 0         unless (lc($symlink_ext) eq lc($target_ext)) {
181 0           push @{ $res->[2] }, "Symlink extension ($symlink_ext) does not match target's ($target_ext)";
  0            
182             }
183             }
184             }
185             CHECK_CONTENT_MATCHES: {
186 0 0         if ($args{content_matches}) {
  0            
187 0           require File::MimeInfo::Magic;
188 0           my ($symlink_ext) = $symlink =~ /\.(\w+)\z/;
189 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            
190 0           my $type = File::MimeInfo::Magic::mimetype($fh);
191 0 0         my @exts; @exts = File::MimeInfo::Magic::extensions($type) if $type;
  0            
192 0 0 0       if (defined($symlink_ext) && @exts) {
    0 0        
193 0           my $found;
194 0           for my $ext (@exts) {
195 0 0         if (lc $ext eq lc $symlink_ext) { $found++; last }
  0            
  0            
196             }
197 0 0         unless ($found) {
198 0           push @{ $res->[2] }, "Symlink extension ($symlink_ext) does not match content type ($type, exts=".join("|", @exts).")";
  0            
199             }
200             } elsif (defined($symlink_ext) xor @exts) {
201 0 0         if (defined $symlink_ext) {
202 0           push @{ $res->[2] }, "Content type is unknown but symlink has extension ($symlink_ext)";
  0            
203             } else {
204 0           push @{ $res->[2] }, "Content type is $type but symlink does not have any extension";
  0            
205             }
206             } else {
207             # mime type is unknown and file does not have extension -> OK
208             }
209             }
210             }
211              
212             END_CHECK:
213 0 0         if (@{ $res->[2] }) { $res->[0] = 500; $res->[1] = "Errors" }
  0            
  0            
  0            
214 0           $res;
215             }
216              
217             1;
218             # ABSTRACT: Utilities related to symbolic links
219              
220             __END__