File Coverage

blib/lib/MHFS/Util.pm
Criterion Covered Total %
statement 162 295 54.9
branch 25 76 32.8
condition 8 25 32.0
subroutine 36 57 63.1
pod 0 34 0.0
total 231 487 47.4


line stmt bran cond sub pod time code
1             package MHFS::Util v0.7.0;
2 2     2   178877 use 5.014;
  2         9  
3 2     2   12 use strict; use warnings;
  2     2   5  
  2         53  
  2         9  
  2         2  
  2         97  
4 2     2   11 use feature 'say';
  2         4  
  2         276  
5 2     2   11 use Carp qw(croak);
  2         5  
  2         121  
6 2     2   11 use Exporter 'import';
  2         4  
  2         76  
7 2     2   10 use Feature::Compat::Try;
  2         3  
  2         20  
8 2     2   160 use File::Find;
  2         4  
  2         138  
9 2     2   12 use File::Basename;
  2         5  
  2         136  
10 2     2   497 use POSIX ();
  2         6715  
  2         72  
11 2     2   13 use Cwd qw(abs_path getcwd);
  2         5  
  2         153  
12 2     2   13 use Encode qw(decode encode);
  2         3  
  2         93  
13 2     2   436 use URI::Escape qw(uri_escape uri_escape_utf8);
  2         1823  
  2         156  
14 2     2   1568 use MIME::Base64 qw(encode_base64url decode_base64url);
  2         1917  
  2         150  
15 2     2   1071 use PerlIO::encoding;
  2         1146  
  2         112  
16 2     2   15 use warnings::register;
  2         4  
  2         496  
17             our @EXPORT_OK = ('LOCK_GET_LOCKDATA', 'LOCK_WRITE', 'UNLOCK_WRITE', 'write_file', 'write_text_file', 'write_text_file_lossy', 'read_file', 'read_text_file', 'read_text_file_lossy', 'shellcmd_unlock', 'ASYNC', 'FindFile', 'space2us', 'escape_html', 'shell_escape', 'pid_running', 'escape_html_noquote', 'output_dir_versatile', 'do_multiples', 'getMIME', 'get_printable_utf8', 'small_url_encode', 'uri_escape_path', 'uri_escape_path_utf8', 'round', 'ceil_div', 'get_SI_size', 'str_to_base64url', 'base64url_to_str', 'decode_utf_8', 'parse_ipv4', 'fold_case');
18              
19             BEGIN {
20 2 50   2   260 if (eval "use feature 'fc'; 1;") {
  2     2   16  
  2         4  
  2         74  
21 2         10521 *fold_case = \&CORE::fc;
22             } else {
23 0         0 *fold_case = \&lc;
24             }
25             }
26              
27             # single threaded locks
28             sub LOCK_GET_LOCKDATA {
29 0     0 0 0 my ($filename) = @_;
30 0         0 my $lockname = "$filename.lock";
31 0         0 try { read_text_file($lockname) }
  0         0  
32 0         0 catch ($e) { return; }
33             }
34              
35             #sub LOCK_GET_FILESIZE {
36             # my ($filename) = @_;
37             # my $lockedfilesize = LOCK_GET_LOCKDATA($filename);
38             # if(defined $lockedfilesize) {
39             #
40             # }
41             #}
42              
43             sub LOCK_WRITE {
44 0     0 0 0 my ($filename, $lockdata) = @_;
45 0         0 my $lockname = "$filename.lock";
46 0 0       0 if(-e $lockname) {
47 0         0 return 0;
48             }
49 0   0     0 $lockdata //= "99999999999"; #99 Billion
50 0         0 write_text_file($lockname, $lockdata);
51 0         0 return 1;
52             }
53              
54             sub UNLOCK_WRITE {
55 0     0 0 0 my ($filename) = @_;
56 0         0 my $lockname = "$filename.lock";
57 0         0 unlink($lockname);
58             }
59              
60             sub write_file {
61 2     2 0 1514 my ($filename, $data) = @_;
62 2 100       11 if (utf8::is_utf8($data)) {
63 1         93 warnings::warnif "UTF8 string in write_file";
64 1         5 Encode::_utf8_off($data);
65             }
66 2 50       349 open (my $fh, '>', $filename) or croak "$! $filename";
67 2         29 print $fh $data;
68 2         157 close($fh);
69             }
70              
71             sub write_text_file {
72 1     1 0 639 my ($filename, $text) = @_;
73 1         4 local $PerlIO::encoding::fallback = Encode::FB_CROAK;
74 1 50       186 open (my $fh, '>:encoding(UTF-8)', $filename) or croak "$! $filename";
75 1         129 print $fh $text;
76 1         52 close($fh);
77             }
78              
79             sub write_text_file_lossy {
80 1     1 0 532 my ($filename, $text) = @_;
81 1         4 local $PerlIO::encoding::fallback = Encode::ONLY_PRAGMA_WARNINGS | Encode::WARN_ON_ERR;
82 1 50       191 open (my $fh, '>:encoding(UTF-8)', $filename) or croak "$! $filename";
83 1         108 print $fh $text;
84 1         21 close($fh);
85             }
86              
87             sub read_file {
88 2     2 0 17 my ($filename) = @_;
89 2         12 local $/ = undef;
90 2 50       86 open my $fh, "<", $filename or croak "Failed to open $filename";
91 2   33     131 <$fh> // croak "Error reading from $filename"
92             }
93              
94             sub read_text_file {
95 2     2 0 756 my ($filename) = @_;
96 2         11 local $/ = undef;
97 2         4 local $PerlIO::encoding::fallback = Encode::FB_CROAK;
98 2 50       100 open my $fh, '<:encoding(UTF-8)', $filename or croak "Failed to open $filename";
99 2   33     254 <$fh> // croak "Error reading from $filename"
100             }
101              
102             sub read_text_file_lossy {
103 1     1 0 1014 my ($filename) = @_;
104 1         7 local $/ = undef;
105 1         3 local $PerlIO::encoding::fallback = Encode::ONLY_PRAGMA_WARNINGS | Encode::WARN_ON_ERR;
106 1 50       68 open my $fh, '<:encoding(UTF-8)', $filename or croak "Failed to open $filename";
107 1   33     171 <$fh> // croak "Error reading from $filename"
108             }
109              
110             # This is not fast
111             sub FindFile {
112 0     0 0 0 my ($directories, $name_req, $path_req) = @_;
113 0         0 my $curdir = getcwd();
114 0         0 my $foundpath;
115 0         0 eval {
116 0         0 my $dir_matches = 1;
117             my %options = ('wanted' => sub {
118 0 0   0   0 return if(! $dir_matches);
119 0 0       0 if(/$name_req/i) {
120 0 0       0 return if( -d );
121 0         0 $foundpath = $File::Find::name;
122 0         0 die;
123             }
124 0         0 });
125              
126 0 0       0 if(defined $path_req) {
127             $options{'preprocess'} = sub {
128 0     0   0 $dir_matches = ($File::Find::dir =~ /$path_req/i);
129 0         0 return @_;
130 0         0 };
131             }
132              
133              
134 0         0 find(\%options, @$directories);
135             };
136 0         0 chdir($curdir);
137 0         0 return $foundpath;
138             }
139              
140             sub shellcmd_unlock {
141 0     0 0 0 my ($command_arr, $fullpath) = @_;
142 0         0 system @$command_arr;
143 0         0 UNLOCK_WRITE($fullpath);
144             }
145              
146             sub ASYNC {
147 0     0 0 0 my $func = shift;
148 0         0 my $pid = fork();
149 0 0       0 if($pid == 0) {
150 0         0 $func->(@_);
151             #exit 0;
152 0         0 POSIX::_exit(0);
153             }
154             else {
155 0         0 say "PID $pid ASYNC";
156 0         0 return $pid;
157             }
158             }
159              
160             sub space2us {
161 1     1 0 245709 my ($string) = @_;
162 1         9 $string =~ s/\s/_/g;
163 1         9 return $string;
164             }
165             sub escape_html {
166 1     1 0 4 my ($string) = @_;
167 1         11 my %dangerchars = ( '"' => '"', "'" => ''', '<' => '<', '>' => '>', '/' => '/');
168 1         5 $string =~ s/&/&/g;
169 1         5 foreach my $key(keys %dangerchars) {
170 5         14 my $val = $dangerchars{$key};
171 5         72 $string =~ s/$key/$val/g;
172             }
173 1         10 return \$string;
174             }
175              
176             sub escape_html_noquote {
177 1     1 0 4 my ($string) = @_;
178 1         5 my %dangerchars = ('<' => '<', '>' => '>');
179 1         4 $string =~ s/&/&/g;
180 1         4 foreach my $key(keys %dangerchars) {
181 2         6 my $val = $dangerchars{$key};
182 2         34 $string =~ s/$key/$val/g;
183             }
184 1         7 return \$string;
185             }
186              
187             sub pid_running {
188 0     0 0 0 return kill 0, shift;
189             }
190              
191             sub shell_escape {
192 1     1 0 3 my ($cmd) = @_;
193 1         6 ($cmd) =~ s/'/'"'"'/g;
194 1         7 return $cmd;
195             }
196              
197             sub output_dir_versatile {
198 0     0 0 0 my ($path, $options) = @_;
199             # hide the root path if desired
200 0         0 my $root = $options->{'root'};
201 0   0     0 $options->{'min_file_size'} //= 0;
202              
203 0         0 my @files;
204 0         0 ON_DIR:
205             # get the list of files and sort
206             my $dir;
207 0 0       0 if(! opendir($dir, $path)) {
208 0         0 warn "outputdir: Cannot open directory: $path $!";
209 0         0 return;
210             }
211 0         0 my @newfiles = sort { uc($a) cmp uc($b)} (readdir $dir);
  0         0  
212 0         0 closedir($dir);
213 0         0 my @newpaths = ();
214 0         0 foreach my $file (@newfiles) {
215 0 0       0 next if($file =~ /^..?$/);
216 0         0 push @newpaths, "$path/$file";
217             }
218 0         0 @files = (@newpaths, @files);
219 0         0 while(@files)
220             {
221 0         0 $path = shift @files;
222 0 0       0 if(! defined $path) {
223 0 0       0 $options->{'on_dir_end'}->() if($options->{'on_dir_end'});
224 0         0 next;
225             }
226 0         0 my $file = basename($path);
227 0 0       0 if(-d $path) {
228 0 0       0 $options->{'on_dir_start'}->($path, $file) if($options->{'on_dir_start'});
229 0         0 @files = (undef, @files);
230 0         0 goto ON_DIR;
231             }
232              
233 0         0 my $unsafePath = $path;
234 0 0       0 if($root) {
235 0         0 $unsafePath =~ s/^$root(\/)?//;
236             }
237 0         0 my $size = -s $path;
238 0 0       0 if(! defined $size) {
239 0         0 say "size not defined path $path file $file";
240 0         0 next;
241             }
242 0 0       0 next if( $size < $options->{'min_file_size'});
243 0 0       0 $options->{'on_file'}->($path, $unsafePath, $file) if($options->{'on_file'});
244             }
245 0         0 return;
246             }
247              
248             # perform multiple async actions at the same time.
249             # continue on with $result_func on failure or completion of all actions
250             sub do_multiples {
251 0     0 0 0 my ($multiples, $result_func) = @_;
252 0         0 my %data;
253 0         0 my @mkeys = keys %{$multiples};
  0         0  
254 0         0 foreach my $multiple (@mkeys) {
255             my $multiple_cb = sub {
256 0     0   0 my ($res) = @_;
257 0         0 $data{$multiple} = $res;
258             # return failure if this multiple failed
259 0 0       0 if(! defined $data{$multiple}) {
260 0         0 $result_func->(undef);
261 0         0 return;
262             }
263             # yield if not all the results in
264 0         0 foreach my $m2 (@mkeys) {
265 0 0       0 return if(! defined $data{$m2});
266             }
267             # all results in we can continue
268 0         0 $result_func->(\%data);
269 0         0 };
270 0         0 say "launching multiple key: $multiple";
271 0         0 $multiples->{$multiple}->($multiple_cb);
272             }
273             }
274              
275             sub getMIME {
276 0     0 0 0 my ($filename) = @_;
277              
278 0         0 my %combined = (
279             # audio
280             'mp3' => 'audio/mp3',
281             'flac' => 'audio/flac',
282             'opus' => 'audio',
283             'ogg' => 'audio/ogg',
284             'wav' => 'audio/wav',
285             # video
286             'mp4' => 'video/mp4',
287             'ts' => 'video/mp2t',
288             'mkv' => 'video/x-matroska',
289             'webm' => 'video/webm',
290             'flv' => 'video/x-flv',
291             # media
292             'mpd' => 'application/dash+xml',
293             'm3u8' => 'application/x-mpegURL',
294             'm3u8_v' => 'application/x-mpegURL',
295             # text
296             'html' => 'text/html; charset=utf-8',
297             'json' => 'application/json',
298             'js' => 'application/javascript',
299             'txt' => 'text/plain; charset=utf-8',
300             'css' => 'text/css',
301             # images
302             'jpg' => 'image/jpeg',
303             'jpeg' => 'image/jpeg',
304             'png' => 'image/png',
305             'gif' => 'image/gif',
306             'bmp' => 'image/bmp',
307             # binary
308             'pdf' => 'application/pdf',
309             'tar' => 'application/x-tar',
310             'wasm' => 'application/wasm',
311             'bin' => 'application/octet-stream'
312             );
313              
314 0         0 my ($ext) = $filename =~ /\.([^.]+)$/;
315              
316             # default to binary
317 0   0     0 return $combined{$ext} // $combined{'bin'};
318             }
319              
320             sub parse_ipv4 {
321 4     4 0 896 my ($ipstring) = @_;
322 4         9 my $failmessage = "invalid ip: $ipstring";
323 4         37 my @values = $ipstring =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
324 4 100       17 if(scalar(@values) != 4) {
325 1         199 croak $failmessage;
326             }
327 3         10 foreach my $i (0..3) {
328 9 100       151 ($values[$i] <= 255) or croak $failmessage;
329             }
330 2         16 return ($values[0] << 24) | ($values[1] << 16) | ($values[2] << 8) | ($values[3]);
331             }
332              
333             sub surrogatepairtochar {
334 3     3 0 2055 my ($hi, $low) = @_;
335 3         12 my $codepoint = 0x10000 + (ord($hi) - 0xD800) * 0x400 + (ord($low) - 0xDC00);
336 3         21 return pack('U', $codepoint);
337             }
338              
339             sub surrogatecodepointpairtochar {
340 4     4 0 1551 my ($hi, $low) = @_;
341 4         11 my $codepoint = 0x10000 + ($hi - 0xD800) * 0x400 + ($low - 0xDC00);
342 4         23 return pack('U', $codepoint);
343             }
344              
345             # returns the byte length and the codepoint
346             sub _peek_utf8_codepoint {
347 35     35   36379 my ($octets) = @_;
348 35         131 my @rules = (
349             [0x80, 0x00, 1], # 1 byte sequence
350             [0xE0, 0xC0, 2], # 2 byte sequence
351             [0xF0, 0xE0, 3], # 3 byte sequence
352             [0XF8, 0xF0, 4] # 4 byte sequence
353             );
354 35         83 my $byteval = ord(substr($octets, 0, 1));
355 35         85 my $charlen;
356 35         77 foreach my $rule (@rules) {
357 104 100       288 if(($byteval & $rule->[0]) == $rule->[1]) {
358 32         48 $charlen = $rule->[2];
359 32         64 last;
360             }
361             }
362 35 100       90 $charlen or return {'codepoint' => 0xFFFD, 'bytelength' => 1};
363 32         46 my $valid_bytes = 1;
364 32         107 for my $i (1 .. $charlen - 1) {
365             # this handles length($octets) < $charlen properly
366 60         126 my $cont_byte = ord(substr($octets, $i, 1));
367 60 100       128 if (($cont_byte & 0xC0) != 0x80) {
368 12         75 return {'codepoint' => 0xFFFD, 'bytelength' => $valid_bytes};
369             }
370 48         81 $valid_bytes++;
371             }
372 20         177 my $char = decode("utf8", substr($octets, 0, $charlen));
373 20 50       677 if(length($char) > 1) {
374 0         0 warnings::warnif "impossible situation, decode returned more than one char";
375 0         0 return {'codepoint' => 0xFFFD, 'bytelength' => 1};
376             }
377 20         140 return { 'codepoint' => ord($char), 'bytelength' => $charlen};
378             }
379              
380             sub get_printable_utf8 {
381 8     8 0 4179 my ($octets) = @_;
382 8         13 my $res;
383 8         25 while(length($octets)) {
384 13         90 $res .= decode('UTF-8', $octets, Encode::FB_QUIET);
385 13 100       705 last if(!length($octets));
386              
387             # by default replace with the replacement char
388 8         24 my $char = _peek_utf8_codepoint($octets);
389 8         17 my $toappend = chr(0xFFFD);
390 8         49 my $toremove = $char->{bytelength};
391              
392             # if we find a surrogate pair, make the actual codepoint
393 8         12 my $mask = ~0 << 16 | 0xFC00;
394 8 50 66     56 if (length($octets) >= 6 && ($char->{bytelength} == 3) && (($char->{codepoint} & $mask) == 0xD800)) {
      66        
395 1         5 my $secondchar = _peek_utf8_codepoint(substr($octets, 3, 3));
396 1 50 33     9 if(($secondchar->{bytelength} == 3) && (($secondchar->{codepoint} & $mask) == 0xDC00)) {
397 1         5 $toappend = surrogatecodepointpairtochar($char->{codepoint}, $secondchar->{codepoint});
398 1         4 $toremove += 3;
399             }
400             }
401              
402 8         17 $res .= $toappend;
403 8         32 substr($octets, 0, $toremove, '');
404             }
405              
406 8         74 return $res;
407             }
408              
409             # save space by not precent encoding valid UTF-8 characters
410             sub small_url_encode {
411 0     0 0 0 my ($octets) = @_;
412 0         0 say "before $octets";
413              
414 0         0 my $escapedoctets = ${escape_html($octets)};
  0         0  
415 0         0 my $res;
416 0         0 while(length($escapedoctets)) {
417 0         0 $res .= decode('UTF-8', $escapedoctets, Encode::FB_QUIET);
418 0 0       0 last if(!length($escapedoctets));
419 0         0 my $oct = ord(substr($escapedoctets, 0, 1, ''));
420 0         0 $res .= sprintf ("%%%02X", $oct);
421             }
422 0         0 say "now: $res";
423 0         0 return $res;
424             }
425              
426             sub uri_escape_path {
427 0     0 0 0 my ($b_path) = @_;
428 0         0 uri_escape($b_path, qr/[^A-Za-z0-9\-\._~\/]/)
429             }
430              
431             sub uri_escape_path_utf8 {
432 0     0 0 0 my ($path) = @_;
433 0         0 uri_escape_utf8($path, qr/[^A-Za-z0-9\-\._~\/]/)
434             }
435              
436             sub round {
437 0     0 0 0 return int($_[0]+0.5);
438             }
439              
440             sub ceil_div {
441 0     0 0 0 return int(($_[0] + $_[1] - 1) / $_[1]);
442             }
443              
444             sub get_SI_size {
445 0     0 0 0 my ($bytes) = @_;
446 0         0 my $mebibytes = ($bytes / 1048576);
447 0 0       0 if($mebibytes >= 1024) {
448 0         0 return sprintf("%.2f GiB", $bytes / 1073741824);
449             }
450             else {
451 0         0 return sprintf("%.2f MiB", $mebibytes);
452             }
453             }
454              
455             # does not check for valid UTF-8
456             sub str_to_base64url {
457 0     0 0 0 my ($str) = @_;
458 0         0 utf8::encode($str);
459 0         0 encode_base64url($str)
460             }
461              
462             sub base64url_to_str {
463 0     0 0 0 my ($base64url) = @_;
464 0         0 my $bstr = decode_base64url($base64url);
465 0         0 decode('UTF-8', $bstr, Encode::FB_CROAK)
466             }
467              
468             sub die2croak {
469             local $SIG{__DIE__} = sub {
470 1     1   60 my ($message) = @_;
471 1         3 chomp $message;
472 1         13 $message =~ s/\sat\s.+\sline\s\d+\.$//;
473 1         3 local $Carp::CarpLevel;
474 1 50       4 if ($Carp::Verbose) {
475 0         0 $Carp::CarpLevel += 2;
476             }
477 1         249 croak $message;
478 2     2 0 24 };
479 2         5 my $call = shift @_;
480 2         12 &$call;
481             }
482              
483             sub decode_utf_8 {
484             #local $SIG{__DIE__} = sub {
485             # my ($message) = @_;
486             # chomp $message;
487             # $message =~ s/\sat\s.+\sline\s\d+\.$//;
488             # local $Carp::CarpLevel;
489             # $Carp::CarpLevel++ if ($Carp::Verbose);
490             # croak $message;
491             #};
492             #decode('UTF-8', $_[0], Encode::FB_CROAK | Encode::LEAVE_SRC)
493 2     2 0 1967 die2croak(\&decode, 'UTF-8', $_[0], Encode::FB_CROAK | Encode::LEAVE_SRC)
494             }
495              
496             1;