File Coverage

blib/lib/WWW/MLite.pm
Criterion Covered Total %
statement 39 223 17.4
branch 0 104 0.0
condition 0 95 0.0
subroutine 13 30 43.3
pod 15 15 100.0
total 67 467 14.3


line stmt bran cond sub pod time code
1             package WWW::MLite; # $Id: MLite.pm 50 2019-06-21 21:05:37Z minus $
2 1     1   66243 use strict;
  1         11  
  1         28  
3 1     1   601 use utf8;
  1         15  
  1         5  
4              
5             =encoding utf-8
6              
7             =head1 NAME
8              
9             WWW::MLite - Lite Web Application Framework
10              
11             =head1 VERSION
12              
13             Version 2.01
14              
15             =head1 SYNOPSIS
16              
17             package MyApp;
18              
19             use base qw/WWW::MLite/;
20              
21             use HTTP::Status qw/:constants/;
22             use Data::Dumper;
23              
24             __PACKAGE__->register_method( # GET /myapp
25             name => "getIndex",
26             description => "Index page",
27             method => "GET",
28             path => "/myapp",
29             deep => 0,
30             attrs => {
31             foo => 'blah-blah-blah',
32             bar => 'on',
33             deserialize => 0,
34             serialize => 1,
35             },
36             requires => undef,
37             returns => undef,
38             code => sub {
39             my $self = shift;
40             my @params = @_;
41              
42             $self->data(Dumper({
43             params => [@params],
44             name => $self->name,
45             description => $self->info("description"),
46             attrs => $self->info("attrs"),
47             path => $self->info("path"),
48             method => $self->info("method"),
49             requires => $self->info("requires"),
50             returns => $self->info("returns"),
51             }));
52              
53             return HTTP_OK; # HTTP RC
54             });
55              
56             1;
57              
58             package main;
59              
60             use FindBin qw/$Bin/;
61             use lib "$Bin/../lib";
62              
63             use CGI;
64             use File::Spec;
65              
66             my $q = new CGI;
67             my $server = MyApp->new(
68             project => "MyApp",
69             ident => "myapp",
70             root => File::Spec->catdir($Bin, "conf"),
71             #confopts => {... Config::General options ...},
72             configfile => File::Spec->catfile($Bin, "conf", "myapp.conf"),
73             log => "on",
74             logfd => fileno(STDERR),
75             #logfile => '/path/to/log/file.log',
76             nph => 0, # NPH (no-parsed-header)
77             );
78             print $server->call($q->request_method, $q->request_uri, $q) or die($server->error);
79              
80             =head1 DESCRIPTION
81              
82             Lite Web Application Framework
83              
84             This module allows you to quickly and easily write a REST servers
85              
86             =head2 new
87              
88             my $server = MyApp->new(
89             project => "MyApp",
90             ident => "myapp",
91             root => File::Spec->catdir($Bin, "conf"),
92             #confopts => {... Config::General options ...},
93             configfile => File::Spec->catfile($Bin, "conf", "myapp.conf"),
94             log => "on",
95             logfd => fileno(STDERR),
96             #logfile => '/path/to/log/file.log',
97             nph => 0, # NPH (no-parsed-header)
98             );
99              
100             Returns CTK object as WWW::MLite server
101              
102             =over 4
103              
104             =item confopts
105              
106             Optional value. L options
107              
108             =item configfile
109              
110             File of configuration
111              
112             Default: /etc/myapp/myapp.conf
113              
114             =item log
115              
116             General switch for logging enable/disable
117              
118             Default: off
119              
120             Also see configuration for logging manage
121              
122             =item logfd
123              
124             File descriptor or fileno
125              
126             Default: none (use syslog)
127              
128             See L
129              
130             =item logfile
131              
132             Log file path. Not recommended!
133              
134             =item nph
135              
136             Enable or disable NPH mode (no-parsed-header)
137              
138             Default: 0
139              
140             See L
141              
142             This option for the response subroutine only!
143              
144             =item root
145              
146             Root directory for project. This is NOT document root directory!
147              
148             Default: /etc/myapp
149              
150             =back
151              
152             See also L and L
153              
154             =head1 METHODS
155              
156             List of available methods
157              
158             =head2 call
159              
160             See L
161              
162             =head2 call_method
163              
164             $server->call_method( $ENV{REQUEST_URI}, $ENV{REQUEST_METHOD}, ... );
165              
166             Runs the callback function from current method with additional parameters
167              
168             Note: any number of parameters can be specified,
169             all of them will be receive in the callback function and in your overridden the response subroutine
170              
171             Returns: response content
172              
173             =head2 check_http_method
174              
175             $server->check_http_method("GET"); # returns 1
176             $server->check_http_method("OPTIONS"); # returns 0
177              
178             Checks the availability of the HTTP method by its name and returns the status
179              
180             =head2 code
181              
182             my $code = $server->code;
183             my $code = $server->code( 500 );
184              
185             Gets/Sets response HTTP code
186              
187             Default: 200 (HTTP_OK)
188              
189             See L
190              
191             =head2 cleanup
192              
193             $server->cleanup;
194              
195             Cleans the all working data and resets it to default values
196              
197             =head2 data
198              
199             my $data = $server->data;
200             $server->data({
201             param1 => "new value",
202             });
203              
204             Gets/Sets working data structure or HTTP content
205              
206             Default: undef
207              
208             See L
209              
210             =head2 head
211              
212             my $head = $server->head;
213             $server->head({
214             "Content-Type" => "text/plain",
215             });
216              
217             Gets/Sets HTTP headers
218              
219             Default: "text/plain"
220              
221             See L
222              
223             =head2 info
224              
225             my $info = $server->info;
226             my $description => $server->info("description");
227             my $attrs = $server->info("attrs");
228             my $path = $server->info("path");
229             my $method = $server>info("method");
230             my $requires = $server->info("requires");
231             my $returns = $server->info("returns");
232              
233             Returns the info structure or info-data of current method
234              
235             =head2 lookup_method
236              
237             my $method = $server->lookup_method($ENV{REQUEST_URI}, $ENV{REQUEST_METHOD});
238              
239             Returns $method structure from hash of registered methods; or undef if method is not registered
240              
241             =head2 message
242              
243             my $message = $server->message;
244             my $message = $server->message( "Internal Server Error" );
245              
246             Gets/Sets response HTTP message
247              
248             Default: "OK"
249              
250             See L
251              
252             =head2 name
253              
254             my $name = $server->name;
255              
256             Returns name of current method. Default: default
257              
258             =head2 register_method
259              
260             use base qw/WWW::MLite/;
261              
262             use HTTP::Status qw/:constants/;
263             use Data::Dumper;
264              
265             __PACKAGE__->register_method( # GET /myapp
266             name => "getIndex",
267             description => "Index page",
268             method => "GET",
269             path => "/myapp",
270             deep => 0,
271             attrs => {
272             foo => 'blah-blah-blah',
273             bar => 'on',
274             deserialize => 0,
275             serialize => 1,
276             },
277             requires => [
278             qw/ user1 user2 userX /
279             ],
280             returns => {
281             type => 'any',
282             },
283             code => sub {
284             my $self = shift;
285             my @params = @_;
286              
287             # ... your method's code here ...
288              
289             return HTTP_OK; # HTTP RC
290             });
291              
292             Registers new method and returns operation status
293              
294             B This is non class method!
295              
296             =over 4
297              
298             =item attrs
299              
300             Sets attributes of the method as hashref
301              
302             Default: {}
303              
304             In the method's code or response method, you can get the attribute values using the $self->info("attrs") method
305              
306             =item code
307              
308             Sets callback function
309              
310             Default: sub { return HTTP::Status::HTTP_OK }
311              
312             This callback function MUST return HTTP status code
313              
314             See L
315              
316             =item deep, depth
317              
318             Enables deeply scanning of path for method lookup. If this param is set to true then the
319             mechanism of the deeply lookuping will be enabled. For example:
320              
321             For registered path /foo with enabled deep lookuping will be matched any another
322             incoming path that begins from /foo prefix: /foo, /foo/bar, /foo/bar/baz and etc.
323              
324             Default: 0
325              
326             =item description
327              
328             Sets the description of method
329              
330             Default: none
331              
332             =item name
333              
334             Sets the name of method
335              
336             Default: default
337              
338             =item method
339              
340             Sets the HTTP method for trapping. Supported: GET, POST, PUT, DELETE.
341              
342             Default: GET
343              
344             =item path
345              
346             Sets the URL's path for trapping
347              
348             Default: /
349              
350             =item requires
351              
352             Array-ref structure that contains list of groups/users or any data for authorization
353              
354             Default: []
355              
356             =item returns
357              
358             Hash-ref structure that contains schema
359              
360             Default: {}
361              
362             See L, L, L
363              
364             =back
365              
366              
367             =head2 middleware
368              
369             The middleware method. Runs before every Your registered methods.
370              
371             You can override this method in Your class.
372              
373             This method MUST returns HTTP status code.
374             If code is a Successful status code (2xx) then Your registered method will called
375              
376             For examle:
377              
378             sub response {
379             my $self = shift;
380             my @params = @_;
381              
382             # . . .
383              
384             return HTTP::Status::HTTP_OK
385             }
386              
387             =head2 response
388              
389             The method for response prepare.
390              
391             You can override this method in Your class.
392              
393             But note! This method MUST returns serialized or plain content for output
394              
395             For examle:
396              
397             sub response {
398             my $self = shift;
399             my @params = @_;
400             my $rc = $self->code; # RC HTTP code (from yuor methods)
401             my $head = $self->head; # HTTP Headers (hashref)
402             my $data = $self->data; # The working data
403             my $msg = $self->message || HTTP::Status::status_message($rc) || "Unknown code";
404              
405             # . . .
406              
407             my @res = (sprintf("Status: %s %s", $rc, $msg));
408             push @res, sprintf("Content-Type: %s", "text/plain; charset=utf-8");
409             push @res, "", $data // "";
410             return join("\015\012", @res);
411             }
412              
413             =head2 again
414              
415             Internal use only!
416              
417             See L
418              
419             =head1 EXAMPLES
420              
421             See all examples on METACPAN website L
422              
423             =head1 HISTORY
424              
425             See C file
426              
427             =head1 TO DO
428              
429             See C file
430              
431             =head1 BUGS
432              
433             * none noted
434              
435             Please report any bugs to https://rt.cpan.org/.
436              
437             =head1 SEE ALSO
438              
439             L, L
440              
441             =head1 AUTHOR
442              
443             Serż Minus (Sergey Lepenkov) L Eabalama@cpan.orgE
444              
445             =head1 COPYRIGHT
446              
447             Copyright (C) 1998-2019 D&D Corporation. All Rights Reserved
448              
449             =head1 LICENSE
450              
451             This program is free software; you can redistribute it and/or
452             modify it under the same terms as Perl itself.
453              
454             See C file and L
455              
456             =cut
457              
458 1     1   111 use vars qw/ $VERSION /;
  1         3  
  1         78  
459             $VERSION = '2.01';
460              
461 1     1   6 use base qw/ CTK /;
  1         2  
  1         552  
462             $CTK::PLUGIN_ALIAS_MAP{log} = "WWW::MLite::Log";
463              
464 1     1   170419 use Storable qw/dclone/; # for dclone
  1         4581  
  1         65  
465 1     1   439 use HTTP::Status qw/ :is /;
  1         4271  
  1         180  
466 1     1   513 use HTTP::Message;
  1         14686  
  1         34  
467 1     1   7 use HTTP::Headers;
  1         2  
  1         22  
468 1     1   426 use HTTP::Response;
  1         2304  
  1         29  
469 1     1   443 use HTTP::Date;
  1         1615  
  1         62  
470 1     1   446 use CTK::ConfGenUtil;
  1         985  
  1         83  
471 1     1   452 use CTK::TFVals qw/ :ALL /;
  1         1907  
  1         258  
472              
473             use constant {
474 1         2565 APP_PLUGINS => [qw/
475             config log
476             /],
477             METHODS => {
478             GET => 1,
479             POST => 1,
480             PUT => 1,
481             DELETE => 1,
482             PATCH => 1,
483             },
484             EOL => "\015\012",
485             KEY_MASK => "%s#%s", # METHOD, PATH
486             REG_KEY_MASK => "%s#%s#%d", # CLASS, SERVER_NAME, SERVER_PORT
487             DEFAULT_METHOD => "GET",
488             DEFAULT_NAME => "default",
489             DEFAULT_PATH => "/", # Root
490             DEFAULT_SERVER_NAME => "localhost",
491             DEFAULT_SERVER_PORT => 80,
492             DEFAULT_CONTENT_TYPE=> "text/plain",
493 1     1   8 };
  1         2  
494              
495             my %method_registry;
496              
497             sub again {
498 0     0 1   my $self = shift;
499 0           my $args = $self->origin;
500 0           my $status = $self->load_plugins(@{(APP_PLUGINS)});
  0            
501 0 0         $self->{status} = 0 unless $status;
502 0           my $config = $self->configobj;
503              
504             # Autoloading logger (data from config)
505 0   0       my $log_on = $config->get("logenable") || $config->get("logenabled") || 0;
506 0 0 0       if ($self->logmode && $log_on) {
507 0   0       my $logopts = $args->{logopts} || {};
508 0 0         my $logfile = defined($args->{logfile}) ? $self->logfile : $config->get("logfile"); # From args or config
509 0 0         $logopts->{facility} = $args->{logfacility} if defined($args->{logfacility}); # From args only!
510 0 0         if ($args->{logfd}) {
511 0           $logopts->{fd} = $args->{logfd};
512             } else {
513 0 0 0       $logopts->{file} = $logfile if defined($logfile) && length($logfile);
514             }
515             $logopts->{ident} = defined($args->{ident})
516             ? $args->{ident}
517 0 0 0       : ($config->get("logident") // $self->project); # From args or config
518             $logopts->{level} = defined($args->{loglevel})
519             ? $args->{loglevel}
520 0 0         : ($config->get("loglevel")); # From args or config
521 0 0         $self->logger_init(%$logopts) or do {
522 0           $self->error("Can't initialize logger");
523 0           $self->{status} = 0;
524             };
525             }
526              
527             # Set methods
528             my $registry_key = sprintf(REG_KEY_MASK,
529             ref($self),
530             $ENV{SERVER_NAME} || DEFAULT_SERVER_NAME,
531 0   0       $ENV{SERVER_PORT} || DEFAULT_SERVER_PORT,
      0        
532             );
533             $self->{methods} = exists($method_registry{$registry_key}) ? $method_registry{$registry_key} : {},
534              
535             # Set name, info, code, head, data
536 0 0         $self->{name} = undef; # Method name
537 0           $self->{info} = undef; # Method info (without code)
538 0           $self->{code} = undef; # Response code (RC)
539 0           $self->{message} = undef; # Response message
540 0           $self->{head} = undef; # Response headers
541 0           $self->{data} = undef; # Response data
542 0           $self->{request_method} = undef; # Request method
543 0           $self->{request_uri} = undef; # Request uri
544              
545 0           return $self;
546             }
547              
548             sub register_method {
549 0     0 1   my $class = shift; # Caller's class
550 0 0         croak("Can't use reference in class name context") if ref($class);
551 0           my %info = @_;
552             my $registry_key = sprintf(REG_KEY_MASK,
553             $class,
554             $ENV{SERVER_NAME} || DEFAULT_SERVER_NAME,
555 0   0       $ENV{SERVER_PORT} || DEFAULT_SERVER_PORT,
      0        
556             );
557 0 0         $method_registry{$registry_key} = {} unless exists($method_registry{$registry_key});
558 0           my $methods = $method_registry{$registry_key};
559              
560             # Method & Path
561 0   0       my $meth = $info{method} || DEFAULT_METHOD;
562 0 0         $meth = DEFAULT_METHOD unless grep {$_ eq $meth} keys %{(METHODS())};
  0            
  0            
563 0   0       my $path = $info{path} // "";
564 0           $path =~ s/\/+$//;
565 0 0         $path = DEFAULT_PATH unless length($path);
566              
567             # Meta
568 0   0       my $name = $info{name} || DEFAULT_NAME;
569 0   0 0     my $code = $info{code} || sub {return HTTP::Status::HTTP_OK};
  0            
570 0 0 0       my $attrs = $info{attrs} && is_hash($info{attrs}) ? $info{attrs} : {};
571 0 0 0       my $returns = $info{returns} && is_hash($info{returns}) ? $info{returns} : {};
572 0   0       my $description = $info{description} || "";
573 0   0       my $deep = $info{deep} || $info{depth} || 0;
574 0   0       my $requires = array($info{requires} || []);
575              
576             # Key
577 0           my $key = sprintf(KEY_MASK, $meth, $path);
578 0 0         if ($methods->{$key}) {
579 0   0       my $tname = $methods->{$key}{name} || DEFAULT_NAME;
580 0 0         return 0 if $tname ne $name;
581             }
582              
583 0           $methods->{$key} = {
584             method => $meth,
585             path => $path,
586             name => $name,
587             code => $code,
588             deep => $deep,
589             requires=> $requires,
590             attrs => $attrs,
591             returns => $returns,
592             description => $description,
593             };
594 0           return 1;
595             }
596             sub check_http_method {
597 0     0 1   my $self = shift;
598 0           my $meth = shift;
599 0 0         return 0 unless $meth;
600 0 0         return 1 if $meth eq 'HEAD';
601 0           my $meths = METHODS;
602 0 0         return $meths->{$meth} ? 1 : 0;
603             }
604              
605             sub name {
606 0     0 1   my $self = shift;
607 0   0       return $self->{name} || DEFAULT_NAME;
608             }
609             sub info {
610 0     0 1   my $self = shift;
611 0           my $name = shift;
612 0   0       my $meta = dclone($self->{info} || {name => $self->name});
613 0 0         return $meta unless defined($name);
614 0 0         return undef unless defined $meta->{$name};
615 0           return $meta->{$name};
616             }
617             sub code {
618 0     0 1   my $self = shift;
619 0           my $value = shift;
620 0 0         return fv2zero($self->{code}) unless defined($value);
621 0   0       $self->{code} = $value || HTTP::Status::HTTP_OK;
622 0           return $self->{code};
623             }
624             sub message {
625 0     0 1   my $self = shift;
626 0           my $value = shift;
627 0 0         return $self->{message} unless defined($value);
628 0   0       $self->{message} = $value || HTTP::Status::status_message(HTTP::Status::HTTP_OK);
629 0           return $self->{message};
630             }
631             sub head {
632 0     0 1   my $self = shift;
633 0           my $struct = shift;
634 0 0         return $self->{head} unless defined($struct);
635 0           $self->{head} = $struct;
636 0           return $struct;
637             }
638             sub data {
639 0     0 1   my $self = shift;
640 0           my $struct = shift;
641 0 0         return $self->{data} unless defined($struct);
642 0           $self->{data} = $struct;
643 0           return $struct;
644             }
645              
646             sub lookup_method {
647 0     0 1   my $self = shift;
648 0           my ($imeth, $ipath) = @_;
649              
650             # Method
651 0   0       my $meth = uc($imeth || DEFAULT_METHOD);
652 0 0         $meth = "GET" if $meth eq 'HEAD';
653 0 0         unless ($self->check_http_method($meth)) {
654 0           $self->error(sprintf("The HTTP %s method not allowed", $meth));
655 0           return undef;
656             }
657              
658             # Path
659 0   0       my $path = $ipath || DEFAULT_PATH;
660 0           $path =~ s/[?\#](.*)$//;
661 0           $path =~ s/\/+$//;
662 0 0         $path = DEFAULT_PATH unless length($path);
663              
664             # Get method
665 0           my $name;
666 0           my $key = sprintf(KEY_MASK, $meth, $path);
667 0           my $methods = $self->{methods};
668             # ...by key
669             return $methods->{$key} if $methods->{$key}
670             && $methods->{$key}{name}
671 0 0 0       && $methods->{$key}{code};
      0        
672             # ...by path
673 0           foreach my $p (_scan_backward($path)) {
674 0           my $ikey = sprintf(KEY_MASK, $meth, $p);
675             return $methods->{$ikey} if $methods->{$ikey}
676             && $methods->{$ikey}{deep}
677             && $methods->{$ikey}{name}
678 0 0 0       && $methods->{$ikey}{code};
      0        
      0        
679             }
680 0           $self->error(sprintf("Method not found (%s %s)", $meth, $path));
681 0           return undef;
682             }
683             sub call_method {
684 0     0 1   my $self = shift;
685 0           my $meth = shift;
686 0           my $path = shift;
687 0           my @params = @_;
688 0           $self->cleanup;
689 0           $self->{request_method} = $meth;
690 0           $self->{request_uri} = $path;
691 0 0         my $method = $self->lookup_method($meth, $path) or return;
692 0 0         unless(ref($method) eq 'HASH') {
693 0           $self->error("Incorrect method structure");
694 0           return;
695             }
696              
697             # Get info
698 0           my %info;
699             my $func;
700 0           foreach my $k (keys %$method) {
701 0 0         next unless defined $k;
702 0 0         if ($k eq 'code') {
    0          
703 0           $func = $method->{code};
704 0           next;
705             } elsif ($k eq 'name') {
706 0           $self->{name} = $method->{name};
707             }
708 0           $info{$k} = $method->{$k};
709             }
710 0           $self->{info} = dclone(\%info);
711              
712             # Call middleware method
713 0           my $rc = $self->middleware(@params);
714              
715             # Call method
716 0 0 0       if ($rc && !is_success($rc)) {
    0          
717             # Skip!
718             } elsif (ref($func) eq 'CODE') {
719 0           $rc = &$func($self, @params);
720             } else {
721 0           $self->message(sprintf("The code of method %s not found!", $self->name));
722 0           $rc = HTTP::Status::HTTP_NOT_IMPLEMENTED;
723             }
724 0           $self->{code} = $rc;
725              
726             # Call response method
727 0 0         unless (HTTP::Status::status_message($rc)) {
728 0           $self->message(sprintf("Method %s returns incorrect HTTP status code!", $self->name));
729 0           $self->{code} = HTTP::Status::HTTP_INTERNAL_SERVER_ERROR;
730             }
731 0           return $self->response(@params);
732             }
733 0     0 1   sub call { goto &call_method }
734              
735             sub cleanup {
736 0     0 1   my $self = shift;
737 0           $self->error(""); # Flush error
738 0           $self->{name} = undef; # Method name
739 0           $self->{info} = undef; # Method info (without code)
740 0           $self->{code} = undef; # Response code (RC)
741 0           $self->{message} = undef; # Response message
742 0           $self->{head} = undef; # Response headers
743 0           $self->{data} = undef; # Response data
744 0           $self->{request_method} = undef; # Request method
745 0           $self->{request_uri} = undef; # Request uri
746 0           return 1;
747             }
748              
749             sub middleware {
750 0     0 1   my $self = shift;
751 0           return HTTP::Status::HTTP_OK;
752             }
753             sub response {
754 0     0 1   my $self = shift;
755 0           my $rc = $self->code;
756 0           my $head = $self->head;
757 0           my $data = $self->data;
758 0   0       my $msg = $self->message || HTTP::Status::status_message($rc) || "Unknown code";
759              
760             # Content
761 0           my $dct = DEFAULT_CONTENT_TYPE;
762 0   0       my $content = $data // "";
763 0 0         $content = "" if $rc =~ /^(1\d\d|[23]04)$/; # make sure content we have no content
764 0 0         if (utf8::is_utf8($content)) {
765 0           utf8::encode($content);
766 0           $dct .= "; charset=utf-8";
767             }
768 0           my $cl = length($content);
769 0 0         $cl += length("\n") if $self->origin->{nph}; # Hack for HTTP::Message::as_string (eol char)
770              
771             # Headers
772 0           my $h = HTTP::Headers->new(Status => sprintf("%s %s", $rc, $msg));
773 0 0         if (is_void($head)) { # No data!
    0          
774 0           $h->header('Server' => sprintf("%s/%s", __PACKAGE__, $VERSION));
775 0           $h->header('Connection' => 'close');
776 0           $h->header('Date' => HTTP::Date::time2str(time()));
777 0           $h->header('Content-Type' => $dct);
778             } elsif (is_hash($head)) { # Data!
779 0           $h->header(%$head);
780             }
781 0 0 0       $h->header('Content-Length' => $cl) if $cl && !$h->header('Content-Length');
782              
783             # Response
784 0 0 0       my $ishead = $self->{request_method} && $self->{request_method} eq 'HEAD' ? 1 : 0;
785 0 0 0       my $r = HTTP::Response->new($rc, $msg, $h, ($cl && !$ishead ? $content : ""));
786              
787             # Done!
788             return $self->origin->{nph}
789             ? $r->as_string
790 0 0 0       : join(EOL, $r->{'_headers'}->as_string(EOL), ($cl && !$ishead ? $content : ""));
    0          
791             }
792              
793             sub _scan_backward { # Returns for /foo/bar/baz array: /foo/bar/baz, /foo/bar, /foo, /
794 0   0 0     my $p = shift // '';
795 0 0 0       my @out = ($p) if length($p) && $p ne '/';
796 0           while ($p =~ s/\/[^\/]+$//) {
797 0 0         push @out, $p if length($p)
798             }
799 0           push @out, '/';
800 0           return @out;
801             }
802              
803             1;
804              
805             __END__