3f33c670f3ff9b8b51bee8af131dc9b9178d3358
[openssl.git] / util / find-doc-nits
1 #! /usr/bin/env perl
2 # Copyright 2002-2019 The OpenSSL Project Authors. All Rights Reserved.
3 #
4 # Licensed under the Apache License 2.0 (the "License").  You may not use
5 # this file except in compliance with the License.  You can obtain a copy
6 # in the file LICENSE in the source distribution or at
7 # https://www.openssl.org/source/license.html
8
9
10 require 5.10.0;
11 use warnings;
12 use strict;
13
14 use Pod::Checker;
15 use File::Find;
16 use File::Basename;
17 use File::Spec::Functions;
18 use Getopt::Std;
19 use lib catdir(dirname($0), "perl");
20 use OpenSSL::Util::Pod;
21
22 # Set to 1 for debug output
23 my $debug = 0;
24
25 # Where to find openssl command
26 my $BLDTOP  = $ENV{BLDTOP} || ".";
27 my $openssl = "$BLDTOP/util/opensslwrap.sh";
28
29 # Options.
30 our($opt_d);
31 our($opt_e);
32 our($opt_s);
33 our($opt_o);
34 our($opt_h);
35 our($opt_l);
36 our($opt_n);
37 our($opt_p);
38 our($opt_u);
39 our($opt_v);
40 our($opt_c);
41
42 # Print usage message and exit.
43 sub help {
44     print <<EOF;
45 Find small errors (nits) in documentation.  Options:
46     -c List undocumented commands and options
47     -d Detailed list of undocumented (implies -u)
48     -e Detailed list of new undocumented (implies -v)
49     -h Print this help message
50     -l Print bogus links
51     -n Print nits in POD pages
52     -o Causes -e/-v to count symbols added since 1.1.1 as new (implies -v)
53     -u Count undocumented functions
54     -v Count new undocumented functions
55 EOF
56     exit;
57 }
58
59 getopts('cdehlnouv');
60
61 help() if $opt_h;
62 $opt_u = 1 if $opt_d;
63 $opt_v = 1 if $opt_o || $opt_e;
64 die "Cannot use both -u and -v"
65     if $opt_u && $opt_v;
66 die "Cannot use both -d and -e"
67     if $opt_d && $opt_e;
68
69 # We only need to check c, l, n, u and v.
70 # Options d, e, o imply one of the above.
71 die "Need one of -[cdehlnouv] flags.\n"
72     unless $opt_c or $opt_l or $opt_n or $opt_u or $opt_v;
73
74
75 my $temp = '/tmp/docnits.txt';
76 my $OUT;
77 my %public;
78 my $status = 0;
79
80 my %mandatory_sections = (
81     '*' => [ 'NAME', 'DESCRIPTION', 'COPYRIGHT' ],
82     1   => [ 'SYNOPSIS', 'OPTIONS' ],
83     3   => [ 'SYNOPSIS', 'RETURN VALUES' ],
84     5   => [ ],
85     7   => [ ]
86 );
87
88
89 # Print error message, set $status.
90 sub err {
91     print join(" ", @_), "\n";
92     $status = 1
93 }
94
95 # Cross-check functions in the NAME and SYNOPSIS section.
96 sub name_synopsis {
97     my $id = shift;
98     my $filename = shift;
99     my $contents = shift;
100
101     # Get NAME section and all words in it.
102     return unless $contents =~ /=head1 NAME(.*)=head1 SYNOPSIS/ms;
103     my $tmp = $1;
104     $tmp =~ tr/\n/ /;
105     err($id, "trailing comma before - in NAME")
106         if $tmp =~ /, *-/;
107     $tmp =~ s/ -.*//g;
108     err($id, "POD markup among the names in NAME")
109         if $tmp =~ /[<>]/;
110     $tmp =~ s/  */ /g;
111     err($id, "missing comma in NAME")
112         if $tmp =~ /[^,] /;
113
114     my $dirname = dirname($filename);
115     my $simplename = basename(basename($filename, ".in"), ".pod");
116     my $foundfilename = 0;
117     my %foundfilenames = ();
118     my %names;
119     foreach my $n ( split ',', $tmp ) {
120         $n =~ s/^\s+//;
121         $n =~ s/\s+$//;
122         err($id, "the name '$n' contains white-space")
123             if $n =~ /\s/;
124         $names{$n} = 1;
125         $foundfilename++ if $n eq $simplename;
126         $foundfilenames{$n} = 1
127             if -f "$dirname/$n.pod" && $n ne $simplename;
128     }
129     err($id, "the following exist as other .pod files:",
130          sort keys %foundfilenames)
131         if %foundfilenames;
132     err($id, "$simplename (filename) missing from NAME section")
133         unless $foundfilename;
134     if ( $filename !~ /internal/ ) {
135         foreach my $n ( keys %names ) {
136             err($id, "$n is not public")
137                 if !defined $public{$n};
138         }
139     }
140
141     # Find all functions in SYNOPSIS
142     return unless $contents =~ /=head1 SYNOPSIS(.*)=head1 DESCRIPTION/ms;
143     my $syn = $1;
144     foreach my $line ( split /\n+/, $syn ) {
145         next unless $line =~ /^\s/;
146         my $sym;
147         my $is_prototype = 1;
148         $line =~ s/STACK_OF\([^)]+\)/int/g;
149         $line =~ s/SPARSE_ARRAY_OF\([^)]+\)/int/g;
150         $line =~ s/__declspec\([^)]+\)//;
151         if ( $line =~ /typedef.*\(\*\S+\)\s+\(/ ) {
152             # a callback function with whitespace before the argument list:
153             # typedef ... (*NAME) (...
154             err($id, "function typedef has space before arg list: $line");
155         }
156         if ( $line =~ /env (\S*)=/ ) {
157             # environment variable env NAME=...
158             $sym = $1;
159         } elsif ( $line =~ /typedef.*\(\*(\S+)\)\s*\(/ ) {
160             # a callback function pointer: typedef ... (*NAME)(...
161             $sym = $1;
162         } elsif ( $line =~ /typedef.* (\S+)\(/ ) {
163             # a callback function signature: typedef ... NAME(...
164             $sym = $1;
165         } elsif ( $line =~ /typedef.* (\S+);/ ) {
166             # a simple typedef: typedef ... NAME;
167             $is_prototype = 0;
168             $sym = $1;
169         } elsif ( $line =~ /enum (\S*) \{/ ) {
170             # an enumeration: enum ... {
171             $sym = $1;
172         } elsif ( $line =~ /#(?:define|undef) ([A-Za-z0-9_]+)/ ) {
173             $is_prototype = 0;
174             $sym = $1;
175         } elsif ( $line =~ /([A-Za-z0-9_]+)\(/ ) {
176             $sym = $1;
177         }
178         else {
179             next;
180         }
181         err($id, "$sym missing from NAME section")
182             unless defined $names{$sym};
183         $names{$sym} = 2;
184
185         # Do some sanity checks on the prototype.
186         err($id, "prototype missing spaces around commas: $line")
187             if $is_prototype && $line =~ /[a-z0-9],[^ ]/;
188     }
189
190     foreach my $n ( keys %names ) {
191         next if $names{$n} == 2;
192         err($id, "$n missing from SYNOPSIS")
193     }
194 }
195
196 # Check if SECTION ($3) is located before BEFORE ($4)
197 sub check_section_location {
198     my $id = shift;
199     my $contents = shift;
200     my $section = shift;
201     my $before = shift;
202
203     return unless $contents =~ /=head1 $section/
204         and $contents =~ /=head1 $before/;
205     err($id, "$section should appear before $before section")
206         if $contents =~ /=head1 $before.*=head1 $section/ms;
207 }
208
209 # Check if a =head1 is duplicated, or a =headX is duplicated within a
210 # =head1.  Treats =head2 =head3 as equivalent -- it doesn't reset the head3
211 # sets if it finds a =head2 -- but that is good enough for now. Also check
212 # for proper capitalization, trailing periods, etc.
213 sub check_head_style {
214     my $id = shift;
215     my $contents = shift;
216     my %head1;
217     my %subheads;
218
219     foreach my $line ( split /\n+/, $contents ) {
220         next unless $line =~ /^=head/;
221         if ( $line =~ /head1/ ) {
222             err($id, "duplicate section $line")
223                 if defined $head1{$line};
224             $head1{$line} = 1;
225             %subheads = ();
226         } else {
227             err($id, "duplicate subsection $line")
228                 if defined $subheads{$line};
229             $subheads{$line} = 1;
230         }
231         err($id, "period in =head")
232             if $line =~ /\.[^\w]/ or $line =~ /\.$/;
233         err($id, "not all uppercase in =head1")
234             if $line =~ /head1.*[a-z]/;
235         err($id, "all uppercase in subhead")
236             if $line =~ /head[234][ A-Z0-9]+$/;
237     }
238 }
239
240 # Because we have options and symbols with extra markup, we need
241 # to take that into account, so we need a regexp that extracts
242 # markup chunks, including recursive markup.
243 # please read up on /(?R)/ in perlre(1)
244 # (note: order is important, (?R) needs to come before .)
245 # (note: non-greedy is important, or something like 'B<foo> and B<bar>'
246 # will be captured as one item)
247 my $markup_re =
248     qr/(                        # Capture group
249            [BIL]<               # The start of what we recurse on
250            (?:(?-1)|.)*?        # recurse the whole regexp (referring to
251                                 # the last opened capture group, i.e. the
252                                 # start of this regexp), or pick next
253                                 # character.  Do NOT be greedy!
254            >                    # The end of what we recurse on
255        )/x;                     # (the x allows this sort of split up regexp)
256
257 # Options must start with a dash, followed by a letter, possibly
258 # followed by letters, digits, dashes and underscores, and the last
259 # character must be a letter or a digit.
260 # We do also accept the single -? or -n, where n is a digit
261 my $option_re =
262     qr/(?:
263             \?                  # Single question mark
264             |
265             \d                  # Single digit
266             |
267             -                   # Single dash (--)
268             |
269             [[:alpha:]](?:[-_[:alnum:]]*?[[:alnum:]])?
270        )/x;
271
272 # Helper function to check if a given $thing is properly marked up
273 # option.  It returns one of these values:
274 #     undef         if it's not an option
275 #     ""            if it's a malformed option
276 #     $unwrapped    the option with the outermost B<> wrapping removed.
277 sub normalise_option {
278     my $id = shift;
279     my $filename = shift;
280     my $thing = shift;
281
282     my $unwrapped = $thing;
283     my $unmarked = $thing;
284
285     # $unwrapped is the option with the outer B<> markup removed
286     $unwrapped =~ s/^B<//;
287     $unwrapped =~ s/>$//;
288     # $unmarked is the option with *all* markup removed
289     $unmarked =~ s/[BIL]<|>//msg;
290
291
292     # If we found an option, check it, collect it
293     if ( $unwrapped =~ /^\s*-/ ) {
294         return $unwrapped       # return option with outer B<> removed
295             if $unmarked =~ /^-${option_re}$/;
296         return "";              # Malformed option
297     }
298     return undef;               # Something else
299 }
300
301 # Checks of command option (man1) formatting.  The man1 checks are
302 # restricted to the SYNOPSIS and OPTIONS sections, the rest is too
303 # free form, we simply cannot be too strict there.
304
305 sub option_check {
306     my $id = shift;
307     my $filename = shift;
308     my $contents = shift;
309
310     my $synopsis = ($contents =~ /=head1\s+SYNOPSIS(.*?)=head1/s, $1);
311
312     # Some pages have more than one OPTIONS section, let's make sure
313     # to get them all
314     my $options = '';
315     while ( $contents =~ /=head1\s+[A-Z ]*?OPTIONS$(.*?)(?==head1)/msg ) {
316         $options .= $1;
317     }
318
319     # Look for options with no or incorrect markup
320     while ( $synopsis =~
321             /(?<![-<[:alnum:]])-(?:$markup_re|.)*(?![->[:alnum:]])/msg ) {
322         err($id, "Malformed option [1] in SYNOPSIS: $&");
323     }
324
325     while ( $synopsis =~ /$markup_re/msg ) {
326         my $found = $&;
327         print STDERR "$id:DEBUG[option_check] SYNOPSIS: found $found\n"
328             if $debug;
329         my $option_uw = normalise_option($id, $filename, $found);
330         err($id, "Malformed option [2] in SYNOPSIS: $found")
331             if defined $option_uw && $option_uw eq '';
332     }
333
334     # In OPTIONS, we look for =item paragraphs.
335     # (?=^\s*$) detects an empty line.
336     while ( $options =~ /=item\s+(.*?)(?=^\s*$)/msg ) {
337         my $item = $&;
338
339         while ( $item =~ /(\[\s*)?($markup_re)/msg ) {
340             my $found = $2;
341             print STDERR "$id:DEBUG[option_check] OPTIONS: found $&\n"
342                 if $debug;
343             err($id, "Unexpected bracket in OPTIONS =item: $item")
344                 if ($1 // '') ne '' && $found =~ /^B<\s*-/;
345
346             my $option_uw = normalise_option($id, $filename, $found);
347             err($id, "Malformed option in OPTIONS: $found")
348                 if defined $option_uw && $option_uw eq '';
349         }
350     }
351 }
352
353 # Normal symbol form
354 my $symbol_re = qr/[[:alpha:]_][_[:alnum:]]*?/;
355
356 # Checks of function name (man3) formatting.  The man3 checks are
357 # easier than the man1 checks, we only check the names followed by (),
358 # and only the names that have POD markup.
359 sub functionname_check {
360     my $id = shift;
361     my $filename = shift;
362     my $contents = shift;
363
364     while ( $contents =~ /($markup_re)\(\)/msg ) {
365         print STDERR "$id:DEBUG[functionname_check] SYNOPSIS: found $&\n"
366             if $debug;
367
368         my $symbol = $1;
369         my $unmarked = $symbol;
370         $unmarked =~ s/[BIL]<|>//msg;
371
372         err($id, "Malformed symbol: $symbol")
373             unless $symbol =~ /^B<.*>$/ && $unmarked =~ /^${symbol_re}$/
374     }
375
376     # We can't do the kind of collecting coolness that option_check()
377     # does, because there are too many things that can't be found in
378     # name repositories like the NAME sections, such as symbol names
379     # with a variable part (typically marked up as B<foo_I<TYPE>_bar>
380 }
381
382 # This is from http://man7.org/linux/man-pages/man7/man-pages.7.html
383 my %preferred_words = (
384     'bitmask'       => 'bit mask',
385     'builtin'       => 'built-in',
386    #'epoch'         => 'Epoch', # handled specially, below
387     'file name'     => 'filename',
388     'file system'   => 'filesystem',
389     'host name'     => 'hostname',
390     'i-node'        => 'inode',
391     'lower case'    => 'lowercase',
392     'lower-case'    => 'lowercase',
393     'non-zero'      => 'nonzero',
394     'path name'     => 'pathname',
395     'pseudo-terminal' => 'pseudoterminal',
396     'reserved port' => 'privileged port',
397     'system port'   => 'privileged port',
398     'realtime'      => 'real-time',
399     'real time'     => 'real-time',
400     'runtime'       => 'run time',
401     'saved group ID'=> 'saved set-group-ID',
402     'saved set-GID' => 'saved set-group-ID',
403     'saved user ID' => 'saved set-user-ID',
404     'saved set-UID' => 'saved set-user-ID',
405     'set-GID'       => 'set-group-ID',
406     'setgid'        => 'set-group-ID',
407     'set-UID'       => 'set-user-ID',
408     'setuid'        => 'set-user-ID',
409     'super user'    => 'superuser',
410     'super-user'    => 'superuser',
411     'super block'   => 'superblock',
412     'super-block'   => 'superblock',
413     'time stamp'    => 'timestamp',
414     'time zone'     => 'timezone',
415     'upper case'    => 'uppercase',
416     'upper-case'    => 'uppercase',
417     'useable'       => 'usable',
418     'userspace'     => 'user space',
419     'user name'     => 'username',
420     'zeroes'        => 'zeros'
421 );
422
423 # Search manpage for words that have a different preferred use.
424 sub wording {
425     my $id = shift;
426     my $contents = shift;
427
428     foreach my $k ( keys %preferred_words ) {
429         # Sigh, trademark
430         next if $k eq 'file system'
431             and $contents =~ /Microsoft Encrypted File System/;
432         err($id, "found '$k' should use '$preferred_words{$k}'")
433             if $contents =~ /\b\Q$k\E\b/i;
434     }
435     err($id, "found 'epoch' should use 'Epoch'")
436         if $contents =~ /\bepoch\b/;
437 }
438
439 # Perform all sorts of nit/error checks on a manpage
440 sub check {
441     my $filename = shift;
442     my $dirname = basename(dirname($filename));
443
444     my $contents = '';
445     {
446         local $/ = undef;
447         open POD, $filename or die "Couldn't open $filename, $!";
448         $contents = <POD>;
449         close POD;
450     }
451
452     my $id = "${filename}:1:";
453     check_head_style($id, $contents);
454
455     # Check ordering of some sections in man3
456     if ( $filename =~ m|man3/| ) {
457         check_section_location($id, $contents, "RETURN VALUES", "EXAMPLES");
458         check_section_location($id, $contents, "SEE ALSO", "HISTORY");
459         check_section_location($id, $contents, "EXAMPLES", "SEE ALSO");
460     }
461
462     # Make sure every link has a section.
463     while ( $contents =~ /$markup_re/msg ) {
464         my $target = $1;
465         next unless $target =~ /^L<(.*)>$/;     # Skip if not L<...>
466         $target = $1;                           # Peal away L< and >
467         $target =~ s/\/[^\/]*$//;               # Peal away possible anchor
468         $target =~ s/.*\|//g;                   # Peal away possible link text
469         next if $target eq '';                  # Skip if links within page, or
470         next if $target =~ /::/;                #   links to a Perl module, or
471         next if $target =~ /^https?:/;          #   is a URL link, or
472         next if $target =~ /\([1357]\)$/;       #   it has a section
473         err($id, "Section missing in $target")
474     }
475     # Check for proper links to commands.
476     while ( $contents =~ /L<([^>]*)\(1\)(?:\/.*)?>/g ) {
477         my $target = $1;
478         next if $target =~ /openssl-?/;
479         next if -f "doc/man1/$target.pod";
480         # TODO: Filter out "foreign manual" links.
481         next if $target =~ /ps|apropos|sha1sum|procmail|perl/;
482         err($id, "Bad command link L<$target(1)>");
483     }
484     # Check for proper in-man-3 API links.
485     while ( $contents =~ /L<([^>]*)\(3\)(?:\/.*)?>/g ) {
486         my $target = $1;
487         err($id, "Bad L<$target>")
488             unless $target =~ /^[_[:alpha:]][_[:alnum:]]*$/
489     }
490
491     unless ( $contents =~ /=for openssl generic/ ) {
492         if ( $filename =~ m|man3/| ) {
493             name_synopsis($id, $filename, $contents);
494             functionname_check($id, $filename, $contents);
495         } elsif ( $filename =~ m|man1/| ) {
496             option_check($id, $filename, $contents)
497         }
498     }
499
500     wording($id, $contents);
501
502     err($id, "doesn't start with =pod")
503         if $contents !~ /^=pod/;
504     err($id, "doesn't end with =cut")
505         if $contents !~ /=cut\n$/;
506     err($id, "more than one cut line.")
507         if $contents =~ /=cut.*=cut/ms;
508     err($id, "EXAMPLE not EXAMPLES section.")
509         if $contents =~ /=head1 EXAMPLE[^S]/;
510     err($id, "WARNING not WARNINGS section.")
511         if $contents =~ /=head1 WARNING[^S]/;
512     err($id, "missing copyright")
513         if $contents !~ /Copyright .* The OpenSSL Project Authors/;
514     err($id, "copyright not last")
515         if $contents =~ /head1 COPYRIGHT.*=head/ms;
516     err($id, "head2 in All uppercase")
517         if $contents =~ /head2\s+[A-Z ]+\n/;
518     err($id, "extra space after head")
519         if $contents =~ /=head\d\s\s+/;
520     err($id, "period in NAME section")
521         if $contents =~ /=head1 NAME.*\.\n.*=head1 SYNOPSIS/ms;
522     err($id, "Duplicate $1 in L<>")
523         if $contents =~ /L<([^>]*)\|([^>]*)>/ && $1 eq $2;
524     err($id, "Bad =over $1")
525         if $contents =~ /=over([^ ][^24])/;
526     err($id, "Possible version style issue")
527         if $contents =~ /OpenSSL version [019]/;
528
529     if ( $contents !~ /=for openssl multiple includes/ ) {
530         # Look for multiple consecutive openssl #include lines
531         # (non-consecutive lines are okay; see man3/MD5.pod).
532         if ( $contents =~ /=head1 SYNOPSIS(.*)=head1 DESCRIPTION/ms ) {
533             my $count = 0;
534             foreach my $line ( split /\n+/, $1 ) {
535                 if ( $line =~ m@include <openssl/@ ) {
536                     err($id, "has multiple includes")
537                         if ++$count == 2;
538                 } else {
539                     $count = 0;
540                 }
541             }
542         }
543     }
544
545     open my $OUT, '>', $temp
546         or die "Can't open $temp, $!";
547     podchecker($filename, $OUT);
548     close $OUT;
549     open $OUT, '<', $temp
550         or die "Can't read $temp, $!";
551     while ( <$OUT> ) {
552         next if /\(section\) in.*deprecated/;
553         print;
554     }
555     close $OUT;
556     unlink $temp || warn "Can't remove $temp, $!";
557
558     # Find what section this page is in; assume 3.
559     my $section = 3;
560     $section = $1 if $dirname =~ /man([1-9])/;
561
562     foreach ( (@{$mandatory_sections{'*'}}, @{$mandatory_sections{$section}}) ) {
563         err($id, "missing $_ head1 section")
564             if $contents !~ /^=head1\s+${_}\s*$/m;
565     }
566 }
567
568 # Parse libcrypto.num, etc., and return sorted list of what's there.
569 sub parsenum {
570     my $file = shift;
571     my @apis;
572
573     open my $IN, '<', $file
574         or die "Can't open $file, $!, stopped";
575
576     while ( <$IN> ) {
577         next if /^#/;
578         next if /\bNOEXIST\b/;
579         my @fields = split();
580         die "Malformed line $_"
581             if scalar @fields != 2 && scalar @fields != 4;
582         push @apis, $fields[0];
583     }
584
585     close $IN;
586
587     return sort @apis;
588 }
589
590 # Parse all the manpages, getting return map of what they document
591 # (by looking at their NAME sections).
592 # Map of links in each POD file; filename => [ "foo(1)", "bar(3)", ... ]
593 my %link_map = ();
594 # Map of names in each POD file; "name(s)" => filename
595 my %name_map = ();
596
597 # Load file of symbol names that we know aren't documented.
598 sub loadmissing($)
599 {
600     my $missingfile = shift;
601     my @missing;
602
603     open FH, $missingfile
604         or die "Can't open $missingfile";
605     while ( <FH> ) {
606         chomp;
607         next if /^#/;
608         push @missing, $_;
609     }
610     close FH;
611
612     for (@missing) {
613         err("$missingfile:", "$_ is documented in $name_map{$_}")
614             if !$opt_o && exists $name_map{$_} && defined $name_map{$_};
615     }
616
617     return @missing;
618 }
619
620 # Check for undocumented macros; ignore those in the "missing" file
621 # and do simple check for #define in our header files.
622 sub checkmacros {
623     my $count = 0;
624     my %seen;
625     my @missing;
626
627     if ( $opt_o ) {
628         @missing = loadmissing('util/missingmacro111.txt');
629     } elsif ( $opt_v ) {
630         @missing = loadmissing('util/missingmacro.txt');
631     }
632
633     foreach my $f ( glob('include/openssl/*.h') ) {
634         # Skip some internals we don't want to document yet.
635         next if $f eq 'include/openssl/asn1.h';
636         next if $f eq 'include/openssl/asn1t.h';
637         next if $f eq 'include/openssl/err.h';
638         open(IN, $f)
639             or die "Can't open $f, $!";
640         while ( <IN> ) {
641             next unless /^#\s*define\s*(\S+)\(/;
642             my $macro = "$1(3)"; # We know they're all in section 3
643             next if exists $name_map{$macro} || defined $seen{$macro};
644             next if $macro =~ /^i2d_/
645                 || $macro =~ /^d2i_/
646                 || $macro =~ /^DEPRECATEDIN/
647                 || $macro =~ /\Q_fnsig(3)\E$/
648                 || $macro =~ /^IMPLEMENT_/
649                 || $macro =~ /^_?DECLARE_/;
650
651             # Skip macros known to be missing
652             next if $opt_v && grep( /^\Q$macro\E$/, @missing);
653
654             err("$f:", "macro $macro undocumented")
655                 if $opt_d || $opt_e;
656             $count++;
657             $seen{$macro} = 1;
658         }
659         close(IN);
660     }
661     err("# $count macros undocumented (count is approximate)")
662         if $count > 0;
663 }
664
665 # Find out what is undocumented (filtering out the known missing ones)
666 # and display them.
667 sub printem {
668     my $libname = shift;
669     my $numfile = shift;
670     my $missingfile = shift;
671     my $count = 0;
672     my %seen;
673
674     my @missing = loadmissing($missingfile) if $opt_v;
675
676     foreach my $func ( parsenum($numfile) ) {
677         $func .= '(3)';         # We know they're all in section 3
678         next if exists $name_map{$func} || defined $seen{$func};
679
680         # Skip functions known to be missing.
681         next if $opt_v && grep( /^\Q$func\E$/, @missing);
682
683         err("$libname:", "function $func undocumented")
684             if $opt_d || $opt_e;
685         $count++;
686         $seen{$func} = 1;
687     }
688     err("# $count in $numfile are not documented")
689         if $count > 0;
690 }
691
692 # Collect all the names in a manpage.
693 sub collectnames {
694     my $filename = shift;
695     $filename =~ m|man(\d)/|;
696     my $section = $1;
697     my $simplename = basename($filename, ".pod");
698     my $id = "${filename}:1:";
699     my %podinfo = extract_pod_info($filename, { debug => $debug });
700
701     unless ( grep { $simplename eq $_ } @{$podinfo{names}} ) {
702         err($id, "$simplename not in NAME section");
703         push @{$podinfo{names}}, $simplename;
704     }
705     foreach my $name ( @{$podinfo{names}} ) {
706         next if $name eq "";
707         err($id, "'$name' contains white space")
708             if $name =~ /\s/;
709         my $name_sec = "$name($section)";
710         if ( !exists $name_map{$name_sec} ) {
711             $name_map{$name_sec} = $filename;
712         } elsif ( $filename eq $name_map{$name_sec} ) {
713             err($id, "$name_sec duplicated in NAME section of",
714                  $name_map{$name_sec});
715         } else {
716             err($id, "$name_sec also in NAME section of",
717                  $name_map{$name_sec});
718         }
719     }
720
721     if ( $podinfo{contents} =~ /=for openssl foreign manual (.*)\n/ ) {
722         foreach my $f ( split / /, $1 ) {
723             $name_map{$f} = undef; # It still exists!
724         }
725     }
726
727     my @links =
728         $podinfo{contents} =~ /L<
729                               # if the link is of the form L<something|name(s)>,
730                               # then remove 'something'.  Note that 'something'
731                               # may contain POD codes as well...
732                               (?:(?:[^\|]|<[^>]*>)*\|)?
733                               # we're only interested in references that have
734                               # a one digit section number
735                               ([^\/>\(]+\(\d\))
736                              /gx;
737     $link_map{$filename} = [ @links ];
738 }
739
740 # Look for L<> ("link") references that point to files that do not exist.
741 sub checklinks {
742     foreach my $filename ( sort keys %link_map ) {
743         foreach my $link ( @{$link_map{$filename}} ) {
744             err("${filename}:1:", "reference to non-existing $link")
745                 unless exists $name_map{$link};
746         }
747     }
748 }
749
750 # Load the public symbol/macro names
751 sub publicize {
752     foreach my $name ( parsenum('util/libcrypto.num') ) {
753         $public{$name} = 1;
754     }
755     foreach my $name ( parsenum('util/libssl.num') ) {
756         $public{$name} = 1;
757     }
758     foreach my $name ( parsenum('util/other.syms') ) {
759         $public{$name} = 1;
760     }
761 }
762
763 # Cipher/digests to skip if they show up as "not implemented"
764 # because they are, via the "-*" construct.
765 my %skips = (
766     'aes128' => 1,
767     'aes192' => 1,
768     'aes256' => 1,
769     'aria128' => 1,
770     'aria192' => 1,
771     'aria256' => 1,
772     'camellia128' => 1,
773     'camellia192' => 1,
774     'camellia256' => 1,
775     'des' => 1,
776     'des3' => 1,
777     'idea' => 1,
778     'cipher' => 1,
779     'digest' => 1,
780 );
781
782 # Check the flags of a command and see if everything is in the manpage
783 sub checkflags {
784     my $cmd = shift;
785     my $doc = shift;
786     my %cmdopts;
787     my %docopts;
788     my %localskips;
789
790     # Get the list of options in the command.
791     open CFH, "$openssl list --options $cmd|"
792         or die "Can list options for $cmd, $!";
793     while ( <CFH> ) {
794         chop;
795         s/ .$//;
796         $cmdopts{$_} = 1;
797     }
798     close CFH;
799
800     # Get the list of flags from the synopsis
801     open CFH, "<$doc"
802         or die "Can't open $doc, $!";
803     while ( <CFH> ) {
804         chop;
805         last if /DESCRIPTION/;
806         if ( /=for openssl ifdef (.*)/ ) {
807             foreach my $f ( split / /, $1 ) {
808                 $localskips{$f} = 1;
809             }
810             next;
811         }
812         my $opt;
813         if ( /\[B<-([^ >]+)/ ) {
814             $opt = $1;
815         } elsif ( /^B<-([^ >]+)/ ) {
816             $opt = $1;
817         } else {
818             next;
819         }
820         $opt = $1 if $opt =~ /I<(.*)/;
821         $docopts{$1} = 1;
822     }
823     close CFH;
824
825     # See what's in the command not the manpage.
826     my @undocced = sort grep { !defined $docopts{$_} } keys %cmdopts;
827     foreach ( @undocced ) {
828         next if /-/; # Skip the -- end-of-flags marker
829         err("$doc: undocumented option -$_");
830     }
831
832     # See what's in the command not the manpage.
833     my @unimpl = sort grep { !defined $cmdopts{$_} } keys %docopts;
834     foreach ( @unimpl ) {
835         next if defined $skips{$_} || defined $localskips{$_};
836         err("$doc: $cmd does not implement -$_");
837     }
838 }
839
840 ##
841 ##  MAIN()
842 ##  Do the work requested by the various getopt flags.
843 ##  The flags are parsed in alphabetical order, just because we have
844 ##  to have *some way* of listing them.
845 ##
846
847 if ( $opt_c ) {
848     my @commands = ();
849
850     # Get list of commands.
851     open FH, "$openssl list -1 -commands|"
852         or die "Can't list commands, $!";
853     while ( <FH> ) {
854         chop;
855         push @commands, $_;
856     }
857     close FH;
858
859     # See if each has a manpage.
860     foreach my $cmd ( @commands ) {
861         next if $cmd eq 'help' || $cmd eq 'exit';
862         my $doc = "doc/man1/openssl-$cmd.pod";
863         # Handle "tsget" and "CA.pl" pod pages
864         $doc = "doc/man1/$cmd.pod" if -f "doc/man1/$cmd.pod";
865         if ( ! -f "$doc" ) {
866             err("$doc does not exist");
867         } else {
868             checkflags($cmd, $doc);
869         }
870     }
871
872     # See what help is missing.
873     open FH, "$openssl list --missing-help |"
874         or die "Can't list missing help, $!";
875     while ( <FH> ) {
876         chop;
877         my ($cmd, $flag) = split;
878         err("$cmd has no help for -$flag");
879     }
880     close FH;
881
882     exit $status;
883 }
884
885 # Preparation for some options, populate %name_map and %link_map
886 if ( $opt_l || $opt_u || $opt_v ) {
887     foreach ( glob('doc/*/*.pod doc/internal/*/*.pod') ) {
888         collectnames($_);
889     }
890 }
891
892 if ( $opt_l ) {
893     foreach my $func ( loadmissing("util/missingcrypto.txt") ) {
894         $name_map{$func} = undef;
895     }
896     checklinks();
897 }
898
899 if ( $opt_n ) {
900     publicize();
901     foreach ( @ARGV ? @ARGV : glob('doc/*/*.pod doc/internal/*/*.pod') ) {
902         check($_);
903     }
904
905     # If not given args, check that all man1 commands are named properly.
906     if ( scalar @ARGV == 0 ) {
907         foreach ( glob('doc/man1/*.pod') ) {
908             next if /CA.pl/ || /openssl\.pod/ || /tsget\.pod/;
909             err("$_ doesn't start with openssl-") unless /openssl-/;
910         }
911     }
912 }
913
914 if ( $opt_u || $opt_v) {
915     if ( $opt_o ) {
916         printem('crypto', 'util/libcrypto.num', 'util/missingcrypto111.txt');
917         printem('ssl', 'util/libssl.num', 'util/missingssl111.txt');
918     } else {
919         printem('crypto', 'util/libcrypto.num', 'util/missingcrypto.txt');
920         printem('ssl', 'util/libssl.num', 'util/missingssl.txt');
921     }
922     checkmacros();
923 }
924
925 exit $status;