Rich Salz | 1bc7451 | 2016-05-20 08:11:46 -0400 | [diff] [blame] | 1 | #! /usr/bin/env perl |
Rich Salz | 05ea606 | 2016-05-20 20:52:46 -0400 | [diff] [blame] | 2 | # Copyright 2002-2016 The OpenSSL Project Authors. All Rights Reserved. |
| 3 | # |
| 4 | # Licensed under the OpenSSL license (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 | |
Rich Salz | 1bc7451 | 2016-05-20 08:11:46 -0400 | [diff] [blame] | 9 | |
| 10 | require 5.10.0; |
| 11 | use warnings; |
| 12 | use strict; |
| 13 | use Pod::Checker; |
| 14 | use File::Find; |
Richard Levitte | 169a8e3 | 2016-05-22 01:26:45 +0200 | [diff] [blame] | 15 | use File::Basename; |
Rich Salz | 71a8b85 | 2016-11-13 01:00:44 -0500 | [diff] [blame] | 16 | use File::Spec::Functions; |
Rich Salz | 35ea640 | 2016-06-01 13:10:24 -0400 | [diff] [blame] | 17 | use Getopt::Std; |
Rich Salz | 71a8b85 | 2016-11-13 01:00:44 -0500 | [diff] [blame] | 18 | use lib catdir(dirname($0), "perl"); |
| 19 | use OpenSSL::Util::Pod; |
Rich Salz | 35ea640 | 2016-06-01 13:10:24 -0400 | [diff] [blame] | 20 | |
Rich Salz | 71a8b85 | 2016-11-13 01:00:44 -0500 | [diff] [blame] | 21 | # Options. |
Rich Salz | 35ea640 | 2016-06-01 13:10:24 -0400 | [diff] [blame] | 22 | our($opt_s); |
Rich Salz | 71a8b85 | 2016-11-13 01:00:44 -0500 | [diff] [blame] | 23 | our($opt_u); |
| 24 | our($opt_h); |
| 25 | our($opt_n); |
Rich Salz | 9e183d2 | 2017-03-11 08:56:44 -0500 | [diff] [blame] | 26 | our($opt_l); |
Rich Salz | 71a8b85 | 2016-11-13 01:00:44 -0500 | [diff] [blame] | 27 | |
| 28 | sub help() |
| 29 | { |
| 30 | print <<EOF; |
| 31 | Find small errors (nits) in documentation. Options: |
Rich Salz | 9e183d2 | 2017-03-11 08:56:44 -0500 | [diff] [blame] | 32 | -l Print bogus links |
Rich Salz | 71a8b85 | 2016-11-13 01:00:44 -0500 | [diff] [blame] | 33 | -n Print nits in POD pages |
| 34 | -s Also print missing sections in POD pages (implies -n) |
| 35 | -u List undocumented functions |
| 36 | -h Print this help message |
| 37 | EOF |
| 38 | exit; |
| 39 | } |
Rich Salz | 1bc7451 | 2016-05-20 08:11:46 -0400 | [diff] [blame] | 40 | |
Rich Salz | 05ea606 | 2016-05-20 20:52:46 -0400 | [diff] [blame] | 41 | my $temp = '/tmp/docnits.txt'; |
| 42 | my $OUT; |
| 43 | |
Richard Levitte | 169a8e3 | 2016-05-22 01:26:45 +0200 | [diff] [blame] | 44 | my %mandatory_sections = |
| 45 | ( '*' => [ 'NAME', 'DESCRIPTION', 'COPYRIGHT' ], |
Rich Salz | 3dfda1a | 2016-12-12 11:14:40 -0500 | [diff] [blame] | 46 | 1 => [ 'SYNOPSIS', 'OPTIONS' ], |
| 47 | 3 => [ 'SYNOPSIS', 'RETURN VALUES' ], |
Richard Levitte | 169a8e3 | 2016-05-22 01:26:45 +0200 | [diff] [blame] | 48 | 5 => [ ], |
| 49 | 7 => [ ] ); |
Richard Levitte | 169a8e3 | 2016-05-22 01:26:45 +0200 | [diff] [blame] | 50 | |
Rich Salz | 35ea640 | 2016-06-01 13:10:24 -0400 | [diff] [blame] | 51 | # Cross-check functions in the NAME and SYNOPSIS section. |
| 52 | sub name_synopsis() |
| 53 | { |
| 54 | my $id = shift; |
| 55 | my $filename = shift; |
| 56 | my $contents = shift; |
| 57 | |
Rich Salz | 35ea640 | 2016-06-01 13:10:24 -0400 | [diff] [blame] | 58 | # Get NAME section and all words in it. |
| 59 | return unless $contents =~ /=head1 NAME(.*)=head1 SYNOPSIS/ms; |
| 60 | my $tmp = $1; |
| 61 | $tmp =~ tr/\n/ /; |
| 62 | $tmp =~ s/-.*//g; |
| 63 | $tmp =~ s/,//g; |
Rich Salz | fbba5d1 | 2016-06-07 13:08:20 -0400 | [diff] [blame] | 64 | |
| 65 | my $dirname = dirname($filename); |
| 66 | my $simplename = basename($filename); |
| 67 | $simplename =~ s/.pod$//; |
| 68 | my $foundfilename = 0; |
| 69 | my %foundfilenames = (); |
Rich Salz | 35ea640 | 2016-06-01 13:10:24 -0400 | [diff] [blame] | 70 | my %names; |
| 71 | foreach my $n ( split ' ', $tmp ) { |
| 72 | $names{$n} = 1; |
Rich Salz | fbba5d1 | 2016-06-07 13:08:20 -0400 | [diff] [blame] | 73 | $foundfilename++ if $n eq $simplename; |
| 74 | $foundfilenames{$n} = 1 |
| 75 | if -f "$dirname/$n.pod" && $n ne $simplename; |
Rich Salz | 35ea640 | 2016-06-01 13:10:24 -0400 | [diff] [blame] | 76 | } |
Rich Salz | fbba5d1 | 2016-06-07 13:08:20 -0400 | [diff] [blame] | 77 | print "$id the following exist as other .pod files:\n", |
| 78 | join(" ", sort keys %foundfilenames), "\n" |
| 79 | if %foundfilenames; |
| 80 | print "$id $simplename (filename) missing from NAME section\n", |
| 81 | unless $foundfilename; |
Rich Salz | 35ea640 | 2016-06-01 13:10:24 -0400 | [diff] [blame] | 82 | |
| 83 | # Find all functions in SYNOPSIS |
| 84 | return unless $contents =~ /=head1 SYNOPSIS(.*)=head1 DESCRIPTION/ms; |
| 85 | my $syn = $1; |
| 86 | foreach my $line ( split /\n+/, $syn ) { |
Rich Salz | 8162f6f | 2016-06-09 17:02:59 -0400 | [diff] [blame] | 87 | my $sym; |
Rich Salz | c952780 | 2016-06-21 07:03:34 -0400 | [diff] [blame] | 88 | $line =~ s/STACK_OF\([^)]+\)/int/g; |
| 89 | $line =~ s/__declspec\([^)]+\)//; |
Rich Salz | 121677b | 2016-12-27 15:00:06 -0500 | [diff] [blame] | 90 | if ( $line =~ /env (\S*)=/ ) { |
| 91 | # environment variable env NAME=... |
| 92 | $sym = $1; |
| 93 | } elsif ( $line =~ /typedef.*\(\*(\S+)\)\(.*/ ) { |
| 94 | # a callback function: typedef ... (*NAME)(... |
| 95 | $sym = $1; |
| 96 | } elsif ( $line =~ /typedef.* (\S+);/ ) { |
| 97 | # a simple typedef: typedef ... NAME; |
Rich Salz | 8162f6f | 2016-06-09 17:02:59 -0400 | [diff] [blame] | 98 | $sym = $1; |
Rich Salz | d4ea965 | 2017-03-11 12:48:32 -0500 | [diff] [blame] | 99 | } elsif ( $line =~ /enum (\S*) {/ ) { |
| 100 | # an enumeration: enum ... { |
| 101 | $sym = $1; |
Rich Salz | c952780 | 2016-06-21 07:03:34 -0400 | [diff] [blame] | 102 | } elsif ( $line =~ /#define ([A-Za-z0-9_]+)/ ) { |
Rich Salz | 8162f6f | 2016-06-09 17:02:59 -0400 | [diff] [blame] | 103 | $sym = $1; |
| 104 | } elsif ( $line =~ /([A-Za-z0-9_]+)\(/ ) { |
| 105 | $sym = $1; |
| 106 | } |
| 107 | else { |
| 108 | next; |
| 109 | } |
| 110 | print "$id $sym missing from NAME section\n" |
| 111 | unless defined $names{$sym}; |
| 112 | $names{$sym} = 2; |
Rich Salz | aebb9aa | 2016-07-19 09:27:53 -0400 | [diff] [blame] | 113 | |
| 114 | # Do some sanity checks on the prototype. |
| 115 | print "$id prototype missing spaces around commas: $line\n" |
| 116 | if ( $line =~ /[a-z0-9],[^ ]/ ); |
Rich Salz | 35ea640 | 2016-06-01 13:10:24 -0400 | [diff] [blame] | 117 | } |
| 118 | |
| 119 | foreach my $n ( keys %names ) { |
| 120 | next if $names{$n} == 2; |
| 121 | print "$id $n missing from SYNOPSIS\n"; |
| 122 | } |
| 123 | } |
| 124 | |
Rich Salz | 1bc7451 | 2016-05-20 08:11:46 -0400 | [diff] [blame] | 125 | sub check() |
| 126 | { |
Richard Levitte | 169a8e3 | 2016-05-22 01:26:45 +0200 | [diff] [blame] | 127 | my $filename = shift; |
| 128 | my $dirname = basename(dirname($filename)); |
Rich Salz | 843666f | 2016-06-03 14:49:20 -0400 | [diff] [blame] | 129 | |
Rich Salz | 1bc7451 | 2016-05-20 08:11:46 -0400 | [diff] [blame] | 130 | my $contents = ''; |
| 131 | { |
| 132 | local $/ = undef; |
Richard Levitte | 169a8e3 | 2016-05-22 01:26:45 +0200 | [diff] [blame] | 133 | open POD, $filename or die "Couldn't open $filename, $!"; |
Rich Salz | 1bc7451 | 2016-05-20 08:11:46 -0400 | [diff] [blame] | 134 | $contents = <POD>; |
| 135 | close POD; |
| 136 | } |
Rich Salz | 1bc7451 | 2016-05-20 08:11:46 -0400 | [diff] [blame] | 137 | |
Rich Salz | 843666f | 2016-06-03 14:49:20 -0400 | [diff] [blame] | 138 | my $id = "${filename}:1:"; |
Rich Salz | 35ea640 | 2016-06-01 13:10:24 -0400 | [diff] [blame] | 139 | |
Rich Salz | 4692340 | 2016-06-07 15:49:08 -0400 | [diff] [blame] | 140 | &name_synopsis($id, $filename, $contents) |
Rich Salz | 8162f6f | 2016-06-09 17:02:59 -0400 | [diff] [blame] | 141 | unless $contents =~ /=for comment generic/ |
Rich Salz | 99d63d4 | 2016-10-26 13:56:48 -0400 | [diff] [blame] | 142 | or $filename =~ m@man[157]/@; |
Rich Salz | 35ea640 | 2016-06-01 13:10:24 -0400 | [diff] [blame] | 143 | |
| 144 | print "$id doesn't start with =pod\n" |
Rich Salz | 843666f | 2016-06-03 14:49:20 -0400 | [diff] [blame] | 145 | if $contents !~ /^=pod/; |
Rich Salz | 35ea640 | 2016-06-01 13:10:24 -0400 | [diff] [blame] | 146 | print "$id doesn't end with =cut\n" |
Rich Salz | 843666f | 2016-06-03 14:49:20 -0400 | [diff] [blame] | 147 | if $contents !~ /=cut\n$/; |
Rich Salz | 35ea640 | 2016-06-01 13:10:24 -0400 | [diff] [blame] | 148 | print "$id more than one cut line.\n" |
Rich Salz | 843666f | 2016-06-03 14:49:20 -0400 | [diff] [blame] | 149 | if $contents =~ /=cut.*=cut/ms; |
Rich Salz | 35ea640 | 2016-06-01 13:10:24 -0400 | [diff] [blame] | 150 | print "$id missing copyright\n" |
Rich Salz | 843666f | 2016-06-03 14:49:20 -0400 | [diff] [blame] | 151 | if $contents !~ /Copyright .* The OpenSSL Project Authors/; |
Rich Salz | 35ea640 | 2016-06-01 13:10:24 -0400 | [diff] [blame] | 152 | print "$id copyright not last\n" |
Rich Salz | 843666f | 2016-06-03 14:49:20 -0400 | [diff] [blame] | 153 | if $contents =~ /head1 COPYRIGHT.*=head/ms; |
Rich Salz | 35ea640 | 2016-06-01 13:10:24 -0400 | [diff] [blame] | 154 | print "$id head2 in All uppercase\n" |
Rich Salz | 843666f | 2016-06-03 14:49:20 -0400 | [diff] [blame] | 155 | if $contents =~ /head2\s+[A-Z ]+\n/; |
Rich Salz | 35ea640 | 2016-06-01 13:10:24 -0400 | [diff] [blame] | 156 | print "$id extra space after head\n" |
| 157 | if $contents =~ /=head\d\s\s+/; |
| 158 | print "$id period in NAME section\n" |
| 159 | if $contents =~ /=head1 NAME.*\.\n.*=head1 SYNOPSIS/ms; |
| 160 | print "$id POD markup in NAME section\n" |
| 161 | if $contents =~ /=head1 NAME.*[<>].*=head1 SYNOPSIS/ms; |
Rich Salz | 843666f | 2016-06-03 14:49:20 -0400 | [diff] [blame] | 162 | |
| 163 | # Look for multiple consecutive openssl #include lines. |
| 164 | # Consecutive because of files like md5.pod. Sometimes it's okay |
| 165 | # or necessary, as in ssl/SSL_set1_host.pod |
| 166 | if ( $contents !~ /=for comment multiple includes/ ) { |
| 167 | if ( $contents =~ /=head1 SYNOPSIS(.*)=head1 DESCRIPTION/ms ) { |
| 168 | my $count = 0; |
| 169 | foreach my $line ( split /\n+/, $1 ) { |
| 170 | if ( $line =~ m@include <openssl/@ ) { |
| 171 | if ( ++$count == 2 ) { |
Rich Salz | 35ea640 | 2016-06-01 13:10:24 -0400 | [diff] [blame] | 172 | print "$id has multiple includes\n"; |
Rich Salz | 843666f | 2016-06-03 14:49:20 -0400 | [diff] [blame] | 173 | } |
| 174 | } else { |
| 175 | $count = 0; |
| 176 | } |
| 177 | } |
| 178 | } |
| 179 | } |
| 180 | |
Rich Salz | 35ea640 | 2016-06-01 13:10:24 -0400 | [diff] [blame] | 181 | return unless $opt_s; |
| 182 | |
Rich Salz | 843666f | 2016-06-03 14:49:20 -0400 | [diff] [blame] | 183 | # Find what section this page is in. If run from "." assume |
| 184 | # section 3. |
Rich Salz | 99d63d4 | 2016-10-26 13:56:48 -0400 | [diff] [blame] | 185 | my $section = 3; |
| 186 | $section = $1 if $dirname =~ /man([1-9])/; |
Richard Levitte | 169a8e3 | 2016-05-22 01:26:45 +0200 | [diff] [blame] | 187 | |
| 188 | foreach ((@{$mandatory_sections{'*'}}, @{$mandatory_sections{$section}})) { |
Rich Salz | 3dfda1a | 2016-12-12 11:14:40 -0500 | [diff] [blame] | 189 | print "$id: missing $_ head1 section\n" |
Richard Levitte | 169a8e3 | 2016-05-22 01:26:45 +0200 | [diff] [blame] | 190 | if $contents !~ /^=head1\s+${_}\s*$/m; |
| 191 | } |
| 192 | |
Rich Salz | 35ea640 | 2016-06-01 13:10:24 -0400 | [diff] [blame] | 193 | open my $OUT, '>', $temp |
| 194 | or die "Can't open $temp, $!"; |
Richard Levitte | 169a8e3 | 2016-05-22 01:26:45 +0200 | [diff] [blame] | 195 | podchecker($filename, $OUT); |
Rich Salz | 35ea640 | 2016-06-01 13:10:24 -0400 | [diff] [blame] | 196 | close $OUT; |
| 197 | open $OUT, '<', $temp |
| 198 | or die "Can't read $temp, $!"; |
| 199 | while ( <$OUT> ) { |
| 200 | next if /\(section\) in.*deprecated/; |
| 201 | print; |
| 202 | } |
| 203 | close $OUT; |
| 204 | unlink $temp || warn "Can't remove $temp, $!"; |
Rich Salz | 1bc7451 | 2016-05-20 08:11:46 -0400 | [diff] [blame] | 205 | } |
| 206 | |
Rich Salz | 71a8b85 | 2016-11-13 01:00:44 -0500 | [diff] [blame] | 207 | my %dups; |
Rich Salz | 35ea640 | 2016-06-01 13:10:24 -0400 | [diff] [blame] | 208 | |
Rich Salz | 71a8b85 | 2016-11-13 01:00:44 -0500 | [diff] [blame] | 209 | sub parsenum() |
| 210 | { |
| 211 | my $file = shift; |
| 212 | my @apis; |
| 213 | |
| 214 | open my $IN, '<', $file |
| 215 | or die "Can't open $file, $!, stopped"; |
| 216 | |
| 217 | while ( <$IN> ) { |
| 218 | next if /\bNOEXIST\b/; |
| 219 | next if /\bEXPORT_VAR_AS_FUNC\b/; |
| 220 | push @apis, $1 if /([^\s]+).\s/; |
| 221 | } |
| 222 | |
| 223 | close $IN; |
| 224 | |
| 225 | print "# Found ", scalar(@apis), " in $file\n"; |
| 226 | return sort @apis; |
| 227 | } |
| 228 | |
| 229 | sub getdocced() |
| 230 | { |
| 231 | my $dir = shift; |
| 232 | my %return; |
| 233 | |
| 234 | foreach my $pod ( glob("$dir/*.pod") ) { |
| 235 | my %podinfo = extract_pod_info($pod); |
| 236 | foreach my $n ( @{$podinfo{names}} ) { |
| 237 | $return{$n} = $pod; |
| 238 | print "# Duplicate $n in $pod and $dups{$n}\n" |
| 239 | if defined $dups{$n} && $dups{$n} ne $pod; |
| 240 | $dups{$n} = $pod; |
| 241 | } |
| 242 | } |
| 243 | |
| 244 | return %return; |
| 245 | } |
| 246 | |
| 247 | my %docced; |
| 248 | |
| 249 | sub printem() |
| 250 | { |
| 251 | my $libname = shift; |
| 252 | my $numfile = shift; |
| 253 | my $count = 0; |
| 254 | |
| 255 | foreach my $func ( &parsenum($numfile) ) { |
| 256 | next if $docced{$func}; |
| 257 | |
| 258 | # Skip ASN1 utilities |
| 259 | next if $func =~ /^ASN1_/; |
| 260 | |
| 261 | print "$libname:$func\n"; |
| 262 | $count++; |
| 263 | } |
| 264 | print "# Found $count missing from $numfile\n\n"; |
| 265 | } |
| 266 | |
| 267 | |
Rich Salz | 9e183d2 | 2017-03-11 08:56:44 -0500 | [diff] [blame] | 268 | # Collection of links in each POD file. |
| 269 | # filename => [ "foo(1)", "bar(3)", ... ] |
| 270 | my %link_collection = (); |
| 271 | # Collection of names in each POD file. |
| 272 | # "name(s)" => filename |
| 273 | my %name_collection = (); |
| 274 | |
| 275 | sub collectnames { |
| 276 | my $filename = shift; |
| 277 | $filename =~ m|man(\d)/|; |
| 278 | my $section = $1; |
| 279 | my $simplename = basename($filename, ".pod"); |
| 280 | my $id = "${filename}:1:"; |
| 281 | |
| 282 | my $contents = ''; |
| 283 | { |
| 284 | local $/ = undef; |
| 285 | open POD, $filename or die "Couldn't open $filename, $!"; |
| 286 | $contents = <POD>; |
| 287 | close POD; |
| 288 | } |
| 289 | |
| 290 | $contents =~ /=head1 NAME([^=]*)=head1 /ms; |
| 291 | my $tmp = $1; |
| 292 | unless (defined $tmp) { |
| 293 | print "$id weird name section\n"; |
| 294 | return; |
| 295 | } |
| 296 | $tmp =~ tr/\n/ /; |
| 297 | $tmp =~ s/-.*//g; |
| 298 | |
| 299 | my @names = map { s/\s+//g; $_ } split(/,/, $tmp); |
| 300 | unless (grep { $simplename eq $_ } @names) { |
| 301 | print "$id missing $simplename\n"; |
| 302 | push @names, $simplename; |
| 303 | } |
| 304 | foreach my $name (@names) { |
| 305 | next if $name eq ""; |
| 306 | my $name_sec = "$name($section)"; |
| 307 | if (! exists $name_collection{$name_sec}) { |
| 308 | $name_collection{$name_sec} = $filename; |
| 309 | } else { #elsif ($filename ne $name_collection{$name_sec}) { |
| 310 | print "$id $name_sec also in $name_collection{$name_sec}\n"; |
| 311 | } |
| 312 | } |
| 313 | |
| 314 | my @foreign_names = |
| 315 | map { map { s/\s+//g; $_ } split(/,/, $_) } |
| 316 | $contents =~ /=for\s+comment\s+foreign\s+manuals:\s*(.*)\n\n/; |
| 317 | foreach (@foreign_names) { |
| 318 | $name_collection{$_} = undef; # It still exists! |
| 319 | } |
| 320 | |
| 321 | my @links = $contents =~ /L< |
| 322 | # if the link is of the form L<something|name(s)>, |
| 323 | # then remove 'something'. Note that 'something' |
| 324 | # may contain POD codes as well... |
| 325 | (?:(?:[^\|]|<[^>]*>)*\|)? |
| 326 | # we're only interested in referenses that have |
| 327 | # a one digit section number |
| 328 | ([^\/>\(]+\(\d\)) |
| 329 | /gx; |
| 330 | $link_collection{$filename} = [ @links ]; |
| 331 | } |
| 332 | |
| 333 | sub checklinks { |
| 334 | foreach my $filename (sort keys %link_collection) { |
| 335 | foreach my $link (@{$link_collection{$filename}}) { |
| 336 | print "${filename}:1: reference to non-existing $link\n" |
| 337 | unless exists $name_collection{$link}; |
| 338 | } |
| 339 | } |
| 340 | } |
| 341 | |
| 342 | getopts('lnshu'); |
Rich Salz | 71a8b85 | 2016-11-13 01:00:44 -0500 | [diff] [blame] | 343 | |
| 344 | &help() if ( $opt_h ); |
| 345 | |
Rich Salz | 9e183d2 | 2017-03-11 08:56:44 -0500 | [diff] [blame] | 346 | die "Need one of -l -n -s or -u flags.\n" |
| 347 | unless $opt_l or $opt_n or $opt_s or $opt_u; |
Rich Salz | 3dfda1a | 2016-12-12 11:14:40 -0500 | [diff] [blame] | 348 | |
Rich Salz | 71a8b85 | 2016-11-13 01:00:44 -0500 | [diff] [blame] | 349 | if ( $opt_n or $opt_s ) { |
| 350 | foreach (@ARGV ? @ARGV : glob('doc/*/*.pod')) { |
| 351 | &check($_); |
| 352 | } |
| 353 | } |
Rich Salz | 9e183d2 | 2017-03-11 08:56:44 -0500 | [diff] [blame] | 354 | |
| 355 | if ( $opt_l ) { |
| 356 | foreach (@ARGV ? @ARGV : glob('doc/*/*.pod')) { |
| 357 | collectnames($_); |
| 358 | } |
| 359 | checklinks(); |
| 360 | } |
| 361 | |
Rich Salz | 71a8b85 | 2016-11-13 01:00:44 -0500 | [diff] [blame] | 362 | if ( $opt_u ) { |
| 363 | my %temp = &getdocced('doc/man3'); |
| 364 | foreach ( keys %temp ) { |
| 365 | $docced{$_} = $temp{$_}; |
| 366 | } |
| 367 | &printem('crypto', 'util/libcrypto.num'); |
| 368 | &printem('ssl', 'util/libssl.num'); |
Rich Salz | 1bc7451 | 2016-05-20 08:11:46 -0400 | [diff] [blame] | 369 | } |
Rich Salz | 05ea606 | 2016-05-20 20:52:46 -0400 | [diff] [blame] | 370 | |
Rich Salz | 35ea640 | 2016-06-01 13:10:24 -0400 | [diff] [blame] | 371 | exit; |