2016-02-03 21:47:48 +00:00
#!/usr/bin/perl
use strict ;
use warnings ;
my $topbip = 9999 ;
2016-12-15 05:04:28 +00:00
my $include_layer = 1 ;
2016-02-03 21:47:48 +00:00
my %RequiredFields = (
BIP => undef ,
Title => undef ,
2025-04-11 20:08:17 -07:00
Authors => undef ,
2016-02-03 21:47:48 +00:00
Status => undef ,
Type => undef ,
2025-10-08 14:59:36 -07:00
Assigned => undef ,
2016-11-30 09:47:31 +00:00
# License => undef, (has exceptions)
2016-02-03 21:47:48 +00:00
);
my %MayHaveMulti = (
2025-04-11 20:08:17 -07:00
Authors => undef ,
2025-04-14 11:44:23 -07:00
Deputies => undef ,
2016-11-30 09:47:31 +00:00
'Comments-URI' => undef ,
License => undef ,
2017-10-29 04:04:07 +00:00
'License-Code' => undef ,
2025-04-11 20:07:37 -07:00
'Discussion' => undef ,
2016-02-03 21:47:48 +00:00
);
my %DateField = (
2025-10-08 14:59:36 -07:00
Assigned => undef ,
2016-02-03 21:47:48 +00:00
);
my %EmailField = (
2025-04-11 20:08:17 -07:00
Authors => undef ,
2016-02-03 21:47:48 +00:00
Editor => undef ,
2025-04-14 11:44:23 -07:00
Deputies => undef ,
2016-02-03 21:47:48 +00:00
);
my %MiscField = (
2025-04-14 11:44:23 -07:00
'Deputies' => undef ,
2025-04-11 20:05:13 -07:00
'Comments-URI' => undef ,
2016-11-30 09:47:31 +00:00
'Comments-Summary' => undef ,
2025-04-11 20:07:37 -07:00
'Discussion' => undef ,
2016-10-05 05:50:12 +00:00
'Replaces' => undef ,
2020-01-19 14:24:30 -08:00
'Requires' => undef ,
2025-04-11 18:17:21 -07:00
'Proposed-Replacement' => undef ,
2016-02-03 21:47:48 +00:00
);
2025-07-08 14:49:40 +02:00
my %VersionField = (
'Version' => undef ,
);
2025-07-10 01:36:29 +02:00
my @FieldOrder = qw(
BIP
Layer
Title
Author
Authors
Editor
Deputies
Discussions-To
Comments-Summary
Comments-URI
Status
Type
2025-10-08 14:59:36 -07:00
Assigned
2025-07-10 01:36:29 +02:00
License
License-Code
Discussion
Post-History
Version
Requires
Replaces
Proposed-Replacement
Superseded-By
) ;
2016-02-03 21:47:48 +00:00
2016-11-30 09:45:33 +00:00
my %ValidLayer = (
'Consensus (soft fork)' => undef ,
'Consensus (hard fork)' => undef ,
'Peer Services' => undef ,
'API/RPC' => undef ,
'Applications' => undef ,
);
2016-02-03 21:47:48 +00:00
my %ValidStatus = (
Draft => undef ,
2025-04-11 18:02:31 -07:00
Complete => "background-color: #ffffcf" ,
2025-04-11 18:08:44 -07:00
Deployed => "background-color: #cfffcf" ,
2025-04-11 18:11:55 -07:00
Closed => "background-color: #ffcfcf" ,
2016-02-03 21:47:48 +00:00
);
my %ValidType = (
2025-04-11 18:19:21 -07:00
'Specification' => undef ,
2016-02-03 21:47:48 +00:00
'Informational' => undef ,
'Process' => undef ,
);
2025-04-14 11:45:46 -07:00
my %AcceptableLicenses = (
2016-11-30 09:47:31 +00:00
'BSD-2-Clause' => undef ,
'BSD-3-Clause' => undef ,
'CC0-1.0' => undef ,
2025-10-22 21:32:52 +02:00
'FSFAP' => undef ,
2025-04-14 11:45:46 -07:00
'MIT' => undef ,
2026-09-11 18:19:32 +04:00
'MIT-0' => undef ,
2025-04-14 11:45:46 -07:00
'CC-BY-4.0' => undef ,
2016-11-30 09:47:31 +00:00
'Apache-2.0' => undef ,
'BSL-1.0' => undef ,
2025-04-14 11:45:46 -07:00
);
my %DefinedLicenses = (
%AcceptableLicenses ,
2016-11-30 09:47:31 +00:00
'CC-BY-SA-4.0' => undef ,
'AGPL-3.0' => undef ,
'AGPL-3.0+' => undef ,
'FDL-1.3' => undef ,
'GPL-2.0' => undef ,
'GPL-2.0+' => undef ,
'LGPL-2.1' => undef ,
'LGPL-2.1+' => undef ,
2025-10-22 21:32:52 +02:00
'OPUBL-1.0' => undef ,
# There's no identifier for "Public Domain" in the SPDX list but "PD" is used in some BIPs.
2016-11-30 09:47:31 +00:00
'PD' => undef ,
);
2016-12-15 04:11:06 +00:00
my %GrandfatheredPD = map { $_ => undef } qw(9 36 37 38 42 49 50 60 65 67 69 74 80 81 83 90 99 105 107 109 111 112 113 114 122 124 125 126 130 131 132 133 140 141 142 143 144 146 147 150 151 152) ;
2025-04-14 11:45:46 -07:00
my %GrandfatheredCCBySA = map { $_ => undef } qw(98 116 117 134) ;
2025-10-22 21:29:15 +02:00
my %TolerateMissingLicense = map { $_ => undef } qw(1 10 11 12 13 14 15 16 21 31 33 34 35 39 43 44 45 47 61 64 68 70 71 72 73 101 102 106 120 121 134) ;
2023-03-28 09:23:35 +10:00
my %TolerateTitleTooLong = map { $_ => undef } qw(39 44 45 47 49 60 67 68 69 73 74 75 80 81 99 105 106 109 113 122 126 131 143 145 147 173 327) ;
2016-02-03 21:47:48 +00:00
my %emails ;
my $bipnum = 0 ;
while ( ++ $bipnum <= $topbip ) {
my $fn = sprintf "bip-%04d.mediawiki" , $bipnum ;
2025-02-08 20:04:18 +10:00
my $is_markdown = 0 ;
2024-05-01 17:02:36 -04:00
if ( !- e $fn ) {
$fn = sprintf "bip-%04d.md" , $bipnum ;
2025-02-08 20:04:18 +10:00
$is_markdown = 1 ;
2024-05-01 17:02:36 -04:00
}
2016-02-03 21:47:48 +00:00
- e $fn || next ;
open my $F , "<$fn" ;
2025-02-08 20:04:18 +10:00
if ( $is_markdown ) {
while ( <$F> !~ m[^(?:\xef\xbb\xbf)?```$] ) {
die "No ``` in $fn" if eof $F ;
}
} else {
while ( <$F> !~ m[^(?:\xef\xbb\xbf)?<pre>$] ) {
2016-02-03 21:47:48 +00:00
die "No <pre> in $fn" if eof $F ;
2025-02-08 20:04:18 +10:00
}
2016-02-03 21:47:48 +00:00
}
my %found ;
2026-06-11 23:55:42 +10:00
my ( $title , $authors , $status , $version , $type , $layer );
2025-10-22 20:02:57 +10:00
my ( $field , $val , @field_order , $continuation );
2016-02-03 21:47:48 +00:00
while ( <$F> ) {
2025-02-08 20:04:18 +10:00
last if ( $is_markdown && m[^```$] );
last if ( ! $is_markdown && m[^</pre>$] );
2016-02-03 21:47:48 +00:00
if ( m[^ ([\w-] + ) \ : ( .*\ S ) $] ) {
$field = $1 ;
$val = $2 ;
2025-10-22 20:02:57 +10:00
$continuation = 0 ;
2016-02-03 21:47:48 +00:00
die "Duplicate $field field in $fn" if exists $found { $field };
2017-01-14 07:03:49 +00:00
die "Too many spaces in $fn" if $val =~ /^\s/ ;
2016-02-03 21:47:48 +00:00
} elsif ( m[^ ( +)(.*\S)$] ) {
die "Continuation of non-field in $fn" unless defined $field ;
die "Too many spaces in $fn" if length $1 != 2 + length $field ;
die "Not allowed for multi-value in $fn" unless exists $MayHaveMulti { $field };
$val = $2 ;
2025-10-22 20:02:57 +10:00
$continuation = 1 ;
2016-02-03 21:47:48 +00:00
} else {
die "Bad line in $fn preamble" ;
}
die "Extra spaces in $fn" if $val =~ /^\s/ ;
if ( $field eq 'BIP' ) {
die "$fn claims to be BIP $val" if $val ne $bipnum ;
} elsif ( $field eq 'Title' ) {
$title = $val ;
2017-05-23 13:18:52 +00:00
my $title_len = length ( $title );
2025-04-14 11:45:12 -07:00
die "$fn has too-long Title ($title_len > 50 char max)" if $title_len > 50 and not exists $TolerateTitleTooLong { $bipnum };
2025-04-11 20:08:17 -07:00
} elsif ( $field eq 'Authors' ) {
$val =~ m/^(\S[^<@>]*\S) \<([^@>]*\@[\w.-]+\.\w+)\>$/ or die "Malformed Authors line in $fn" ;
2016-02-03 21:47:48 +00:00
my ( $authorname , $authoremail ) = ( $1 , $2 );
$authoremail =~ s/(?<=\D)$bipnum(?=\D)/<BIPNUM>/g ;
$emails { $authorname } -> { $authoremail } = undef ;
2025-04-11 20:08:17 -07:00
if ( defined $authors ) {
$authors .= ", $authorname" ;
2016-02-03 21:47:48 +00:00
} else {
2025-04-11 20:08:17 -07:00
$authors = $authorname ;
2016-02-03 21:47:48 +00:00
}
} elsif ( $field eq 'Status' ) {
if ( $bipnum == 38 ) { # HACK
$val =~ s/\s+\(.*\)$// ;
}
die "Invalid status in $fn" unless exists $ValidStatus { $val };
$status = $val ;
} elsif ( $field eq 'Type' ) {
die "Invalid type in $fn" unless exists $ValidType { $val };
if ( defined $ValidType { $val }) {
$type = $ValidType { $val };
} else {
$type = $val ;
}
2016-11-30 09:45:33 +00:00
} elsif ( $field eq 'Layer' ) { # BIP 123
die "Invalid layer $val in $fn" unless exists $ValidLayer { $val };
$layer = $val ;
2017-10-29 04:04:07 +00:00
} elsif ( $field =~ /^License(?:\-Code)?$/ ) {
2025-10-22 20:02:57 +10:00
die "License continued across lines in $fn, use SPDX expression (... OR $val) instead" if $continuation ;
$val =~ s/ OR .*// ;
2016-11-30 09:47:31 +00:00
die "Undefined license $val in $fn" unless exists $DefinedLicenses { $val };
2017-10-29 04:04:07 +00:00
if ( not $found { $field }) {
2025-04-14 11:45:46 -07:00
die "Unacceptable license $val in $fn" unless exists $AcceptableLicenses { $val } or ( $val eq 'PD' and exists $GrandfatheredPD { $bipnum }) or ( $val eq 'CC-BY-SA-4.0' and exists $GrandfatheredCCBySA { $bipnum });
2016-11-30 09:47:31 +00:00
}
} elsif ( $field eq 'Comments-URI' ) {
2026-08-03 18:44:34 +08:00
if ( not $found { 'Comments-URI' }) {
2017-05-07 22:24:47 +00:00
my $first_comments_uri = sprintf ( 'https://github.com/bitcoin/bips/wiki/Comments:BIP-%04d' , $bipnum );
die "First Comments-URI must be exactly \"$first_comments_uri\" in $fn" unless $val eq $first_comments_uri ;
2016-11-30 09:47:31 +00:00
}
2016-02-03 21:47:48 +00:00
} elsif ( exists $DateField { $field }) {
2025-10-03 18:13:15 +02:00
# Enforce date format 20XX-MM-DD, where XX is 00-99, MM is 01-12 and DD is 01-31
die "Invalid date format in $fn" unless $val =~ /^20\d{2}\-(?:0[1-9]|1[0-2])\-(?:0[1-9]|[12]\d|30|31)$/ ;
2016-02-03 21:47:48 +00:00
} elsif ( exists $EmailField { $field }) {
2026-09-11 17:26:17 +04:00
$val =~ m/^(\S[^<@>]*\S) \<[^@>]*\@[\w.-]+\.\w+\>$/ or die "Malformed $field line in $fn" ;
2025-07-08 14:49:40 +02:00
} elsif ( exists $VersionField { $field }) {
$val =~ m/^(\d+\.\d+\.\d+)$/ or die "Malformed $field line in $fn" ;
2026-06-11 23:55:42 +10:00
$version = $val ;
2016-02-03 21:47:48 +00:00
} elsif ( not exists $MiscField { $field }) {
die "Unknown field $field in $fn" ;
}
2016-12-15 04:20:10 +00:00
++ $found { $field };
2025-07-10 01:36:29 +02:00
push @field_order , $field unless @field_order and $field_order [ - 1 ] eq $field ;
2016-02-03 21:47:48 +00:00
}
2016-11-30 09:47:31 +00:00
if ( not $found { License }) {
die "Missing License in $fn" unless exists $TolerateMissingLicense { $bipnum };
}
2016-02-03 21:47:48 +00:00
for my $field ( keys %RequiredFields ) {
die "Missing $field in $fn" unless $found { $field };
}
2025-07-10 01:36:29 +02:00
my @expected_field_order = grep { exists $found { $_ } } @FieldOrder ;
if ( "@expected_field_order" ne "@field_order" ) {
die "Field order is incorrect in $fn, should be:\n\t" . join ( ", " , @expected_field_order ) . "\nbut contains:\n\t" . join ( ", " , @field_order );
}
2016-02-03 21:47:48 +00:00
print "|-" ;
if ( defined $ValidStatus { $status }) {
print " style=\"" . $ValidStatus { $status } . "\"" ;
}
2026-06-11 23:55:42 +10:00
if ( defined $version and $version =~ m/^0+[.]/ ) {
if ( $status eq "Complete" or $status eq "Deployed" ) {
die "$fn marked as $status despite pre-1.0 version ($version)" ;
}
}
2016-02-03 21:47:48 +00:00
print "\n" ;
print "| [[${fn}|${bipnum}]]\n" ;
2016-11-30 09:45:33 +00:00
if ( $include_layer ) {
if ( defined $layer ) {
print "| ${layer}\n" ;
} else {
print "|\n" ;
}
}
2016-02-03 21:47:48 +00:00
print "| ${title}\n" ;
2025-04-11 20:08:17 -07:00
print "| ${authors}\n" ;
2016-02-03 21:47:48 +00:00
print "| ${type}\n" ;
print "| ${status}\n" ;
close $F ;
}
2025-04-11 20:08:17 -07:00
for my $authors ( sort keys %emails ) {
my @emails = sort keys % { $emails { $authors }};
2016-02-03 21:47:48 +00:00
my $email_count = @emails ;
next unless $email_count > 1 ;
2025-04-11 20:08:17 -07:00
warn "NOTE: $authors has $email_count email addresses: @emails\n" ;
2016-02-03 21:47:48 +00:00
}