]> Sergey Matveev's repositories - public-inbox.git/blob - lib/PublicInbox/LeiExternal.pm
imap+nntp: share COMPRESS implementation
[public-inbox.git] / lib / PublicInbox / LeiExternal.pm
1 # Copyright (C) 2020-2021 all contributors <meta@public-inbox.org>
2 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
3
4 # *-external commands of lei
5 package PublicInbox::LeiExternal;
6 use strict;
7 use v5.10.1;
8 use PublicInbox::Config;
9
10 sub externals_each {
11         my ($self, $cb, @arg) = @_;
12         my $cfg = $self->_lei_cfg;
13         my %boost;
14         for my $sec (grep(/\Aexternal\./, @{$cfg->{-section_order}})) {
15                 my $loc = substr($sec, length('external.'));
16                 $boost{$loc} = $cfg->{"$sec.boost"};
17         }
18         return \%boost if !wantarray && !$cb;
19
20         # highest boost first, but stable for alphabetic tie break
21         use sort 'stable';
22         my @order = sort { $boost{$b} <=> $boost{$a} } sort keys %boost;
23         if (ref($cb) eq 'CODE') {
24                 for my $loc (@order) {
25                         $cb->(@arg, $loc, $boost{$loc});
26                 }
27         } elsif (ref($cb) eq 'HASH') {
28                 %$cb = %boost;
29         }
30         @order; # scalar or array
31 }
32
33 sub ext_canonicalize {
34         my $location = $_[-1]; # $_[0] may be $lei
35         if ($location !~ m!\Ahttps?://!) {
36                 PublicInbox::Config::rel2abs_collapsed($location);
37         } else {
38                 require URI;
39                 my $uri = URI->new($location)->canonical;
40                 my $path = $uri->path . '/';
41                 $path =~ tr!/!/!s; # squeeze redundant '/'
42                 $uri->path($path);
43                 $uri->as_string;
44         }
45 }
46
47 # TODO: we will probably extract glob2re into a separate module for
48 # PublicInbox::Filter::Base and maybe other places
49 my %re_map = ( '*' => '[^/]*?', '?' => '[^/]',
50                 '[' => '[', ']' => ']', ',' => ',' );
51
52 sub glob2re {
53         my $re = $_[-1]; # $_[0] may be $lei
54         my $p = '';
55         my $in_bracket = 0;
56         my $qm = 0;
57         my $schema_host_port = '';
58
59         # don't glob URL-looking things that look like IPv6
60         if ($re =~ s!\A([a-z0-9\+]+://\[[a-f0-9\:]+\](?::[0-9]+)?/)!!i) {
61                 $schema_host_port = quotemeta $1; # "http://[::1]:1234"
62         }
63         my $changes = ($re =~ s!(.)!
64                 $re_map{$p eq '\\' ? '' : do {
65                         if ($1 eq '[') { ++$in_bracket }
66                         elsif ($1 eq ']') { --$in_bracket }
67                         elsif ($1 eq ',') { ++$qm } # no change
68                         $p = $1;
69                 }} // do {
70                         $p = $1;
71                         ($p eq '-' && $in_bracket) ? $p : (++$qm, "\Q$p")
72                 }!sge);
73         # bashism (also supported by curl): {a,b,c} => (a|b|c)
74         $changes += ($re =~ s/([^\\]*)\\\{([^,]*,[^\\]*)\\\}/
75                         (my $in_braces = $2) =~ tr!,!|!;
76                         $1."($in_braces)";
77                         /sge);
78         ($changes - $qm) ? $schema_host_port.$re : undef;
79 }
80
81 # get canonicalized externals list matching $loc
82 # $is_exclude denotes it's for --exclude
83 # otherwise it's for --only/--include is assumed
84 sub get_externals {
85         my ($self, $loc, $is_exclude) = @_;
86         return (ext_canonicalize($loc)) if -e $loc;
87         my @m;
88         my @cur = externals_each($self);
89         my $do_glob = !$self->{opt}->{globoff}; # glob by default
90         if ($do_glob && (my $re = glob2re($loc))) {
91                 @m = grep(m!$re!, @cur);
92                 return @m if scalar(@m);
93         } elsif (index($loc, '/') < 0) { # exact basename match:
94                 @m = grep(m!/\Q$loc\E/?\z!, @cur);
95                 return @m if scalar(@m) == 1;
96         } elsif ($is_exclude) { # URL, maybe:
97                 my $canon = ext_canonicalize($loc);
98                 @m = grep(m!\A\Q$canon\E\z!, @cur);
99                 return @m if scalar(@m) == 1;
100         } else { # URL:
101                 return (ext_canonicalize($loc));
102         }
103         if (scalar(@m) == 0) {
104                 die "`$loc' is unknown\n";
105         } else {
106                 die("`$loc' is ambiguous:\n", map { "\t$_\n" } @m, "\n");
107         }
108 }
109
110 sub canonicalize_excludes {
111         my ($lei, $excludes) = @_;
112         my %x;
113         for my $loc (@$excludes) {
114                 my @l = get_externals($lei, $loc, 1);
115                 $x{$_} = 1 for @l;
116         }
117         \%x;
118 }
119
120 # returns an anonymous sub which returns an array of potential results
121 sub complete_url_prepare {
122         my $argv = $_[-1]; # $_[0] may be $lei
123         # Workaround bash word-splitting URLs to ['https', ':', '//' ...]
124         # Maybe there's a better way to go about this in
125         # contrib/completion/lei-completion.bash
126         my $re = '';
127         my $cur = pop(@$argv) // '';
128         if (@$argv) {
129                 my @x = @$argv;
130                 if ($cur eq ':' && @x) {
131                         push @x, $cur;
132                         $cur = '';
133                 }
134                 while (@x > 2 && $x[0] !~ /\A(?:http|nntp|imap)s?\z/i &&
135                                 $x[1] ne ':') {
136                         shift @x;
137                 }
138                 if (@x >= 2) { # qw(https : hostname : 443) or qw(http :)
139                         $re = join('', @x);
140                 } else { # just filter out the flags and hope for the best
141                         $re = join('', grep(!/^-/, @$argv));
142                 }
143                 $re = quotemeta($re);
144         }
145         my $match_cb = sub {
146                 # the "//;" here (for AUTH=ANONYMOUS) interacts badly with
147                 # bash tab completion, strip it out for now since our commands
148                 # work w/o it.  Not sure if there's a better solution...
149                 $_[0] =~ s!//;AUTH=ANONYMOUS\@!//!i;
150                 $_[0] =~ s!;!\\;!g;
151                 # only return the part specified on the CLI
152                 # don't duplicate if already 100% completed
153                 $_[0] =~ /\A$re(\Q$cur\E.*)/ ? ($cur eq $1 ? () : $1) : ()
154         };
155         wantarray ? ($re, $cur, $match_cb) : $match_cb;
156 }
157
158 1;