xref: /openssl/util/perl/OpenSSL/Ordinals.pm (revision da1c088f)
1#! /usr/bin/env perl
2# Copyright 2018-2023 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
9package OpenSSL::Ordinals;
10
11use strict;
12use warnings;
13use Carp;
14use Scalar::Util qw(blessed);
15use OpenSSL::Util;
16
17use constant {
18    # "magic" filters, see the filters at the end of the file
19    F_NAME      => 1,
20    F_NUMBER    => 2,
21};
22
23=head1 NAME
24
25OpenSSL::Ordinals - a private module to read and walk through ordinals
26
27=head1 SYNOPSIS
28
29  use OpenSSL::Ordinals;
30
31  my $ordinals = OpenSSL::Ordinals->new(from => "foo.num");
32  # or alternatively
33  my $ordinals = OpenSSL::Ordinals->new();
34  $ordinals->load("foo.num");
35
36  foreach ($ordinals->items(comparator => by_name()) {
37    print $_->name(), "\n";
38  }
39
40=head1 DESCRIPTION
41
42This is a OpenSSL private module to load an ordinals (F<.num>) file and
43write out the data you want, sorted and filtered according to your rules.
44
45An ordinals file is a file that enumerates all the symbols that a shared
46library or loadable module must export.  Each of them have a unique
47assigned number as well as other attributes to indicate if they only exist
48on a subset of the supported platforms, or if they are specific to certain
49features.
50
51The unique numbers each symbol gets assigned needs to be maintained for a
52shared library or module to stay compatible with previous versions on
53platforms that maintain a transfer vector indexed by position rather than
54by name.  They also help keep information on certain symbols that are
55aliases for others for certain platforms, or that have different forms
56on different platforms.
57
58=head2 Main methods
59
60=over  4
61
62=cut
63
64=item B<new> I<%options>
65
66Creates a new instance of the C<OpenSSL::Ordinals> class.  It takes options
67in keyed pair form, i.e. a series of C<< key => value >> pairs.  Available
68options are:
69
70=over 4
71
72=item B<< from => FILENAME >>
73
74Not only create a new instance, but immediately load it with data from the
75ordinals file FILENAME.
76
77=back
78
79=cut
80
81sub new {
82    my $class = shift;
83    my %opts = @_;
84
85    my $instance = {
86        filename        => undef, # File name registered when loading
87        loaded_maxnum   => 0,     # Highest allocated item number when loading
88        loaded_contents => [],    # Loaded items, if loading there was
89        maxassigned     => 0,     # Current highest assigned item number
90        maxnum          => 0,     # Current highest allocated item number
91        contents        => [],    # Items, indexed by number
92        name2num        => {},    # Name to number dictionary
93        aliases         => {},    # Aliases cache.
94        stats           => {},    # Statistics, see 'sub validate'
95        debug           => $opts{debug},
96    };
97    bless $instance, $class;
98
99    $instance->set_version($opts{version});
100    $instance->load($opts{from}) if defined($opts{from});
101
102    return $instance;
103}
104
105=item B<< $ordinals->load FILENAME >>
106
107Loads the data from FILENAME into the instance.  Any previously loaded data
108is dropped.
109
110Two internal databases are created.  One database is simply a copy of the file
111contents and is treated as read-only.  The other database is an exact copy of
112the first, but is treated as a work database, i.e. it can be modified and added
113to.
114
115=cut
116
117sub load {
118    my $self = shift;
119    my $filename = shift;
120
121    croak "Undefined filename" unless defined($filename);
122
123    my @tmp_contents = ();
124    my %tmp_name2num = ();
125    my $max_assigned = 0;
126    my $max_num = 0;
127    open F, '<', $filename or croak "Unable to open $filename";
128    while (<F>) {
129        s|\R$||;                # Better chomp
130        s|#.*||;
131        next if /^\s*$/;
132
133        my $item = OpenSSL::Ordinals::Item->new(source => $filename, from => $_);
134
135        my $num = $item->number();
136        if ($num eq '?') {
137            $num = ++$max_num;
138        } elsif ($num eq '?+') {
139            $num = $max_num;
140        } else {
141            croak "Disordered ordinals, number sequence restarted"
142                if $max_num > $max_assigned && $num < $max_num;
143            croak "Disordered ordinals, $num < $max_num"
144                if $num < $max_num;
145            $max_assigned = $max_num = $num;
146        }
147
148        $item->intnum($num);
149        push @{$tmp_contents[$num]}, $item;
150        $tmp_name2num{$item->name()} = $num;
151    }
152    close F;
153
154    $self->{contents} = [ @tmp_contents ];
155    $self->{name2num} = { %tmp_name2num };
156    $self->{maxassigned} = $max_assigned;
157    $self->{maxnum} = $max_num;
158    $self->{filename} = $filename;
159
160    # Make a deep copy, allowing {contents} to be an independent work array
161    foreach my $i (1..$max_num) {
162        if ($tmp_contents[$i]) {
163            $self->{loaded_contents}->[$i] =
164                [ map { OpenSSL::Ordinals::Item->new($_) }
165                  @{$tmp_contents[$i]} ];
166        }
167    }
168    $self->{loaded_maxnum} = $max_num;
169    return 1;
170}
171
172=item B<< $ordinals->renumber >>
173
174Renumber any item that doesn't have an assigned number yet.
175
176=cut
177
178sub renumber {
179    my $self = shift;
180
181    my $max_assigned = 0;
182    foreach ($self->items(sort => by_number())) {
183        $_->number($_->intnum()) if $_->number() =~ m|^\?|;
184        if ($max_assigned < $_->number()) {
185            $max_assigned = $_->number();
186        }
187    }
188    $self->{maxassigned} = $max_assigned;
189}
190
191=item B<< $ordinals->rewrite >>
192
193=item B<< $ordinals->rewrite >>, I<%options>
194
195If an ordinals file has been loaded, it gets rewritten with the data from
196the current work database.
197
198If there are more arguments, they are used as I<%options> with the
199same semantics as for B<< $ordinals->items >> described below, apart
200from B<sort>, which is forbidden here.
201
202=cut
203
204sub rewrite {
205    my $self = shift;
206    my %opts = @_;
207
208    $self->write($self->{filename}, %opts);
209}
210
211=item B<< $ordinals->write FILENAME >>
212
213=item B<< $ordinals->write FILENAME >>, I<%options>
214
215Writes the current work database data to the ordinals file FILENAME.
216This also validates the data, see B<< $ordinals->validate >> below.
217
218If there are more arguments, they are used as I<%options> with the
219same semantics as for B<< $ordinals->items >> described next, apart
220from B<sort>, which is forbidden here.
221
222=cut
223
224sub write {
225    my $self = shift;
226    my $filename = shift;
227    my %opts = @_;
228
229    croak "Undefined filename" unless defined($filename);
230    croak "The 'sort' option is not allowed" if $opts{sort};
231
232    $self->validate();
233
234    open F, '>', $filename or croak "Unable to open $filename";
235    foreach ($self->items(%opts, sort => by_number())) {
236        print F $_->to_string(),"\n";
237    }
238    close F;
239    $self->{filename} = $filename;
240    $self->{loaded_maxnum} = $self->{maxnum};
241    return 1;
242}
243
244=item B<< $ordinals->items >> I<%options>
245
246Returns a list of items according to a set of criteria.  The criteria is
247given in form keyed pair form, i.e. a series of C<< key => value >> pairs.
248Available options are:
249
250=over 4
251
252=item B<< sort => SORTFUNCTION >>
253
254SORTFUNCTION is a reference to a function that takes two arguments, which
255correspond to the classic C<$a> and C<$b> that are available in a C<sort>
256block.
257
258=item B<< filter => FILTERFUNCTION >>
259
260FILTERFUNCTION is a reference to a function that takes one argument, which
261is every OpenSSL::Ordinals::Item element available.
262
263=back
264
265=cut
266
267sub items {
268    my $self = shift;
269    my %opts = @_;
270
271    my $comparator = $opts{sort};
272    my $filter = $opts{filter} // sub { 1; };
273
274    my @l = undef;
275    if (ref($filter) eq 'ARRAY') {
276        # run a "magic" filter
277        if    ($filter->[0] == F_NUMBER) {
278            my $index = $filter->[1];
279            @l = $index ? @{$self->{contents}->[$index] // []} : ();
280        } elsif ($filter->[0] == F_NAME) {
281            my $index = $self->{name2num}->{$filter->[1]};
282            @l = $index ? @{$self->{contents}->[$index] // []} : ();
283        } else {
284            croak __PACKAGE__."->items called with invalid filter";
285        }
286    } elsif (ref($filter) eq 'CODE') {
287        @l = grep { $filter->($_) }
288            map { @{$_ // []} }
289            @{$self->{contents}};
290    } else {
291        croak __PACKAGE__."->items called with invalid filter";
292    }
293
294    return sort { $comparator->($a, $b); } @l
295        if (defined $comparator);
296    return @l;
297}
298
299# Put an array of items back into the object after having checked consistency
300# If there are exactly two items:
301# - They MUST have the same number
302# - They MUST have the same version
303# - For platforms, both MUST hold the same ones, but with opposite values
304# - For features, both MUST hold the same ones.
305# - They MUST NOT have identical name, type, numeral, version, platforms, and features
306# If there's just one item, just put it in the slot of its number
307# In all other cases, something is wrong
308sub _putback {
309    my $self = shift;
310    my @items = @_;
311
312    if (scalar @items < 1 || scalar @items > 2) {
313        croak "Wrong number of items: ", scalar @items, "\n ",
314            join("\n ", map { $_->{source}.": ".$_->name() } @items), "\n";
315    }
316    if (scalar @items == 2) {
317        # Collect some data
318        my %numbers = ();
319        my %versions = ();
320        my %features = ();
321        foreach (@items) {
322            $numbers{$_->intnum()} = 1;
323            $versions{$_->version()} = 1;
324            foreach ($_->features()) {
325                $features{$_}++;
326            }
327        }
328
329        # Check that all items we're trying to put back have the same number
330        croak "Items don't have the same numeral: ",
331            join(", ", map { $_->name()." => ".$_->intnum() } @items), "\n"
332            if (scalar keys %numbers > 1);
333        croak "Items don't have the same version: ",
334            join(", ", map { $_->name()." => ".$_->version() } @items), "\n"
335            if (scalar keys %versions > 1);
336
337        # Check that both items run with the same features
338        foreach (@items) {
339        }
340        foreach (keys %features) {
341            delete $features{$_} if $features{$_} == 2;
342        }
343        croak "Features not in common between ",
344            $items[0]->name(), " and ", $items[1]->name(), ":",
345            join(", ", sort keys %features), "\n"
346            if %features;
347
348        # Check for in addition identical name, type, and platforms
349        croak "Duplicate entries for ".$items[0]->name()." from ".
350            $items[0]->source()." and ".$items[1]->source()."\n"
351            if $items[0]->name() eq $items[1]->name()
352            && $items[0]->type() eq $items[1]->type()
353            && $items[0]->platforms() eq $items[1]->platforms();
354
355        # Check that all platforms exist in both items, and have opposite values
356        my @platforms = ( { $items[0]->platforms() },
357                          { $items[1]->platforms() } );
358        foreach my $platform (keys %{$platforms[0]}) {
359            if (exists $platforms[1]->{$platform}) {
360                if ($platforms[0]->{$platform} != !$platforms[1]->{$platform}) {
361                    croak "Platforms aren't opposite: ",
362                        join(", ",
363                             map { my %tmp_h = $_->platforms();
364                                   $_->name().":".$platform
365                                       ." => "
366                                       .$tmp_h{$platform} } @items),
367                        "\n";
368                }
369
370                # We're done with these
371                delete $platforms[0]->{$platform};
372                delete $platforms[1]->{$platform};
373            }
374        }
375        # If there are any remaining platforms, something's wrong
376        if (%{$platforms[0]} || %{$platforms[0]}) {
377            croak "There are platforms not in common between ",
378                $items[0]->name(), " and ", $items[1]->name(), "\n";
379        }
380    }
381    $self->{contents}->[$items[0]->intnum()] = [ @items ];
382}
383
384sub _parse_platforms {
385    my $self = shift;
386    my @defs = @_;
387
388    my %platforms = ();
389    foreach (@defs) {
390        m{^(!)?};
391        my $op = !(defined $1 && $1 eq '!');
392        my $def = $';
393
394        if ($def =~ m{^_?WIN32$})                   { $platforms{$&} = $op; }
395        if ($def =~ m{^__FreeBSD__$})               { $platforms{$&} = $op; }
396# For future support
397#       if ($def =~ m{^__DragonFly__$})             { $platforms{$&} = $op; }
398#       if ($def =~ m{^__OpenBSD__$})               { $platforms{$&} = $op; }
399#       if ($def =~ m{^__NetBSD__$})                { $platforms{$&} = $op; }
400        if ($def =~ m{^OPENSSL_SYS_})               { $platforms{$'} = $op; }
401    }
402
403    return %platforms;
404}
405
406sub _parse_features {
407    my $self = shift;
408    my @defs = @_;
409
410    my %features = ();
411    foreach (@defs) {
412        m{^(!)?};
413        my $op = !(defined $1 && $1 eq '!');
414        my $def = $';
415
416        if ($def =~ m{^ZLIB$})                      { $features{$&} =  $op; }
417        if ($def =~ m{^BROTLI$})                    { $features{$&} =  $op; }
418        if ($def =~ m{^ZSTD$})                      { $features{$&} =  $op; }
419        if ($def =~ m{^OPENSSL_USE_})               { $features{$'} =  $op; }
420        if ($def =~ m{^OPENSSL_NO_})                { $features{$'} = !$op; }
421    }
422
423    return %features;
424}
425
426sub _adjust_version {
427    my $self = shift;
428    my $version = shift;
429    my $baseversion = $self->{baseversion};
430
431    $version = $baseversion
432        if ($baseversion ne '*' && $version ne '*'
433            && cmp_versions($baseversion, $version) > 0);
434
435    return $version;
436}
437
438=item B<< $ordinals->add SOURCE, NAME, TYPE, LIST >>
439
440Adds a new item from file SOURCE named NAME with the type TYPE,
441and a set of C macros in
442LIST that are expected to be defined or undefined to use this symbol, if
443any.  For undefined macros, they each must be prefixed with a C<!>.
444
445If this symbol already exists in loaded data, it will be rewritten using
446the new input data, but will keep the same ordinal number and version.
447If it's entirely new, it will get a '?' and the current default version.
448
449=cut
450
451sub add {
452    my $self = shift;
453    my $source = shift;         # file where item was defined
454    my $name = shift;
455    my $type = shift;           # FUNCTION or VARIABLE
456    my @defs = @_;              # Macros from #ifdef and #ifndef
457                                # (the latter prefixed with a '!')
458
459    # call signature for debug output
460    my $verbsig = "add('$name' , '$type' , [ " . join(', ', @defs) . " ])";
461
462    croak __PACKAGE__."->add got a bad type '$type'"
463        unless $type eq 'FUNCTION' || $type eq 'VARIABLE';
464
465    my %platforms = _parse_platforms(@defs);
466    my %features = _parse_features(@defs);
467
468    my @items = $self->items(filter => f_name($name));
469    my $version = @items ? $items[0]->version() : $self->{currversion};
470    my $intnum = @items ? $items[0]->intnum() : ++$self->{maxnum};
471    my $number = @items ? $items[0]->number() : '?';
472    print STDERR "DEBUG[",__PACKAGE__,":add] $verbsig\n",
473        @items ? map { "\t".$_->to_string()."\n" } @items : "No previous items\n",
474        if $self->{debug};
475    @items = grep { $_->exists() } @items;
476
477    my $new_item =
478        OpenSSL::Ordinals::Item->new( source        => $source,
479                                      name          => $name,
480                                      type          => $type,
481                                      number        => $number,
482                                      intnum        => $intnum,
483                                      version       =>
484                                          $self->_adjust_version($version),
485                                      exists        => 1,
486                                      platforms     => { %platforms },
487                                      features      => [
488                                          grep { $features{$_} } keys %features
489                                      ] );
490
491    push @items, $new_item;
492    print STDERR "DEBUG[",__PACKAGE__,"::add] $verbsig\n", map { "\t".$_->to_string()."\n" } @items
493        if $self->{debug};
494    $self->_putback(@items);
495
496    # If an alias was defined beforehand, add an item for it now
497    my $alias = $self->{aliases}->{$name};
498    delete $self->{aliases}->{$name};
499
500    # For the caller to show
501    my @returns = ( $new_item );
502    push @returns, $self->add_alias($source, $alias->{name}, $name, @{$alias->{defs}})
503        if defined $alias;
504    return @returns;
505}
506
507=item B<< $ordinals->add_alias SOURCE, ALIAS, NAME, LIST >>
508
509Adds an alias ALIAS for the symbol NAME from file SOURCE, and a set of C macros
510in LIST that are expected to be defined or undefined to use this symbol, if any.
511For undefined macros, they each must be prefixed with a C<!>.
512
513If this symbol already exists in loaded data, it will be rewritten using
514the new input data.  Otherwise, the data will just be store away, to wait
515that the symbol NAME shows up.
516
517=cut
518
519sub add_alias {
520    my $self = shift;
521    my $source = shift;
522    my $alias = shift;          # This is the alias being added
523    my $name  = shift;          # For this name (assuming it exists)
524    my @defs = @_;              # Platform attributes for the alias
525
526    # call signature for debug output
527    my $verbsig =
528        "add_alias('$source' , '$alias' , '$name' , [ " . join(', ', @defs) . " ])";
529
530    croak "You're kidding me... $alias == $name" if $alias eq $name;
531
532    my %platforms = _parse_platforms(@defs);
533    my %features = _parse_features(@defs);
534
535    croak "Alias with associated features is forbidden\n"
536        if %features;
537
538    my $f_byalias = f_name($alias);
539    my $f_byname = f_name($name);
540    my @items = $self->items(filter => $f_byalias);
541    foreach my $item ($self->items(filter => $f_byname)) {
542        push @items, $item unless grep { $_ == $item } @items;
543    }
544    @items = grep { $_->exists() } @items;
545
546    croak "Alias already exists ($alias => $name)"
547        if scalar @items > 1;
548    if (scalar @items == 0) {
549        # The item we want to alias for doesn't exist yet, so we cache the
550        # alias and hope the item we're making an alias of shows up later
551        $self->{aliases}->{$name} = { source => $source,
552                                      name => $alias, defs => [ @defs ] };
553
554        print STDERR "DEBUG[",__PACKAGE__,":add_alias] $verbsig\n",
555            "\tSet future alias $alias => $name\n"
556            if $self->{debug};
557        return ();
558    } elsif (scalar @items == 1) {
559        # The rule is that an alias is more or less a copy of the original
560        # item, just with another name.  Also, the platforms given here are
561        # given to the original item as well, with opposite values.
562        my %alias_platforms = $items[0]->platforms();
563        foreach (keys %platforms) {
564            $alias_platforms{$_} = !$platforms{$_};
565        }
566        # We supposedly do now know how to do this...  *ahem*
567        $items[0]->{platforms} = { %alias_platforms };
568
569        my $number =
570            $items[0]->number() =~ m|^\?| ? '?+' : $items[0]->number();
571        my $alias_item = OpenSSL::Ordinals::Item->new(
572            source        => $source,
573            name          => $alias,
574            type          => $items[0]->type(),
575            number        => $number,
576            intnum        => $items[0]->intnum(),
577            version       => $self->_adjust_version($items[0]->version()),
578            exists        => $items[0]->exists(),
579            platforms     => { %platforms },
580            features      => [ $items[0]->features() ]
581           );
582        push @items, $alias_item;
583
584        print STDERR "DEBUG[",__PACKAGE__,":add_alias] $verbsig\n",
585            map { "\t".$_->to_string()."\n" } @items
586            if $self->{debug};
587        $self->_putback(@items);
588
589        # For the caller to show
590        return ( $alias_item->to_string() );
591    }
592    croak "$name has an alias already (trying to add alias $alias)\n",
593        "\t", join(", ", map { $_->name() } @items), "\n";
594}
595
596=item B<< $ordinals->set_version VERSION >>
597
598=item B<< $ordinals->set_version VERSION BASEVERSION >>
599
600Sets the default version for new symbol to VERSION.
601
602If given, BASEVERSION sets the base version, i.e. the minimum version
603for all symbols.  If not given, it will be calculated as follows:
604
605=over 4
606
607If the given version is '*', then the base version will also be '*'.
608
609If the given version starts with '0.', the base version will be '0.0.0'.
610
611If the given version starts with '1.0.', the base version will be '1.0.0'.
612
613If the given version starts with '1.1.', the base version will be '1.1.0'.
614
615If the given version has a first number C<N> that's greater than 1, the
616base version will be formed from C<N>: 'N.0.0'.
617
618=back
619
620=cut
621
622sub set_version {
623    my $self = shift;
624    # '*' is for "we don't care"
625    my $version = shift // '*';
626    my $baseversion = shift // '*';
627
628    if ($baseversion eq '*') {
629        $baseversion = $version;
630        if ($baseversion ne '*') {
631            if ($baseversion =~ m|^(\d+)\.|, $1 > 1) {
632                $baseversion = "$1.0.0";
633            } else {
634                $baseversion =~ s|^0\..*$|0.0.0|;
635                $baseversion =~ s|^1\.0\..*$|1.0.0|;
636                $baseversion =~ s|^1\.1\..*$|1.1.0|;
637
638                die 'Invalid version'
639                    if ($baseversion ne '0.0.0'
640                        && $baseversion !~ m|^1\.[01]\.0$|);
641            }
642        }
643    }
644
645    die 'Invalid base version'
646        if ($baseversion ne '*' && $version ne '*'
647            && cmp_versions($baseversion, $version) > 0);
648
649    $self->{currversion} = $version;
650    $self->{baseversion} = $baseversion;
651    foreach ($self->items(filter => sub { $_[0] eq '*' })) {
652        $_->{version} = $self->{currversion};
653    }
654    return 1;
655}
656
657=item B<< $ordinals->invalidate >>
658
659Invalidates the whole working database.  The practical effect is that all
660symbols are set to not exist, but are kept around in the database to retain
661ordinal numbers and versions.
662
663=cut
664
665sub invalidate {
666    my $self = shift;
667
668    foreach (@{$self->{contents}}) {
669        foreach (@{$_ // []}) {
670            $_->{exists} = 0;
671        }
672    }
673    $self->{stats} = {};
674}
675
676=item B<< $ordinals->validate >>
677
678Validates the current working database by collection statistics on how many
679symbols were added and how many were changed.  These numbers can be retrieved
680with B<< $ordinals->stats >>.
681
682=cut
683
684sub validate {
685    my $self = shift;
686
687    $self->{stats} = {};
688    for my $i (1..$self->{maxnum}) {
689        if ($i > $self->{loaded_maxnum}
690                || (!@{$self->{loaded_contents}->[$i] // []}
691                    && @{$self->{contents}->[$i] // []})) {
692            $self->{stats}->{new}++;
693        }
694        if ($i <= $self->{maxassigned}) {
695            $self->{stats}->{assigned}++;
696        } else {
697            $self->{stats}->{unassigned}++;
698        }
699        next if ($i > $self->{loaded_maxnum});
700
701        my @loaded_strings =
702            map { $_->to_string() } @{$self->{loaded_contents}->[$i] // []};
703        my @current_strings =
704            map { $_->to_string() } @{$self->{contents}->[$i] // []};
705
706        foreach my $str (@current_strings) {
707            @loaded_strings = grep { $str ne $_ } @loaded_strings;
708        }
709        if (@loaded_strings) {
710            $self->{stats}->{modified}++;
711        }
712    }
713}
714
715=item B<< $ordinals->stats >>
716
717Returns the statistics that B<validate> calculate.
718
719=cut
720
721sub stats {
722    my $self = shift;
723
724    return %{$self->{stats}};
725}
726
727=back
728
729=head2 Data elements
730
731Data elements, which is each line in an ordinals file, are instances
732of a separate class, OpenSSL::Ordinals::Item, with its own methods:
733
734=over 4
735
736=cut
737
738package OpenSSL::Ordinals::Item;
739
740use strict;
741use warnings;
742use Carp;
743
744=item B<new> I<%options>
745
746Creates a new instance of the C<OpenSSL::Ordinals::Item> class.  It takes
747options in keyed pair form, i.e. a series of C<< key => value >> pairs.
748Available options are:
749
750=over 4
751
752=item B<< source => FILENAME >>, B<< from => STRING >>
753
754This will create a new item from FILENAME, filled with data coming from STRING.
755
756STRING must conform to the following EBNF description:
757
758  ordinal string = symbol, spaces, ordinal, spaces, version, spaces,
759                   exist, ":", platforms, ":", type, ":", features;
760  spaces         = space, { space };
761  space          = " " | "\t";
762  symbol         = ( letter | "_" ), { letter | digit | "_" };
763  ordinal        = number | "?" | "?+";
764  version        = number, "_", number, "_", number, [ letter, [ letter ] ];
765  exist          = "EXIST" | "NOEXIST";
766  platforms      = platform, { ",", platform };
767  platform       = ( letter | "_" ) { letter | digit | "_" };
768  type           = "FUNCTION" | "VARIABLE";
769  features       = feature, { ",", feature };
770  feature        = ( letter | "_" ) { letter | digit | "_" };
771  number         = digit, { digit };
772
773(C<letter> and C<digit> are assumed self evident)
774
775=item B<< source => FILENAME >>, B<< name => STRING >>, B<< number => NUMBER >>,
776      B<< version => STRING >>, B<< exists => BOOLEAN >>, B<< type => STRING >>,
777      B<< platforms => HASHref >>, B<< features => LISTref >>
778
779This will create a new item with data coming from the arguments.
780
781=back
782
783=cut
784
785sub new {
786    my $class = shift;
787
788    if (ref($_[0]) eq $class) {
789        return $class->new( map { $_ => $_[0]->{$_} } keys %{$_[0]} );
790    }
791
792    my %opts = @_;
793
794    croak "No argument given" unless %opts;
795
796    my $instance = undef;
797    if ($opts{from}) {
798        my @a = split /\s+/, $opts{from};
799
800        croak "Badly formatted ordinals string: $opts{from}"
801            unless ( scalar @a == 4
802                     && $a[0] =~ /^[A-Za-z_][A-Za-z_0-9]*$/
803                     && $a[1] =~ /^\d+|\?\+?$/
804                     && $a[2] =~ /^(?:\*|\d+_\d+_\d+[a-z]{0,2})$/
805                     && $a[3] =~ /^
806                                  (?:NO)?EXIST:
807                                  [^:]*:
808                                  (?:FUNCTION|VARIABLE):
809                                  [^:]*
810                                  $
811                                 /x );
812
813        my @b = split /:/, $a[3];
814        %opts = ( source        => $opts{source},
815                  name          => $a[0],
816                  number        => $a[1],
817                  version       => $a[2],
818                  exists        => $b[0] eq 'EXIST',
819                  platforms     => { map { m|^(!)?|; $' => !$1 }
820                                         split /,/,$b[1] },
821                  type          => $b[2],
822                  features      => [ split /,/,$b[3] // '' ] );
823    }
824
825    if ($opts{name} && $opts{version} && defined $opts{exists} && $opts{type}
826            && ref($opts{platforms} // {}) eq 'HASH'
827            && ref($opts{features} // []) eq 'ARRAY') {
828        my $version = $opts{version};
829        $version =~ s|_|.|g;
830
831        $instance = { source    => $opts{source},
832                      name      => $opts{name},
833                      type      => $opts{type},
834                      number    => $opts{number},
835                      intnum    => $opts{intnum},
836                      version   => $version,
837                      exists    => !!$opts{exists},
838                      platforms => { %{$opts{platforms} // {}} },
839                      features  => [ sort @{$opts{features} // []} ] };
840    } else {
841        croak __PACKAGE__."->new() called with bad arguments\n".
842            join("", map { "    $_\t=> ".$opts{$_}."\n" } sort keys %opts);
843    }
844
845    return bless $instance, $class;
846}
847
848sub DESTROY {
849}
850
851=item B<< $item->name >>
852
853The symbol name for this item.
854
855=item B<< $item->number >> (read-write)
856
857The positional number for this item.
858
859This may be '?' for an unassigned symbol, or '?+' for an unassigned symbol
860that's an alias for the previous symbol.  '?' and '?+' must be properly
861handled by the caller.  The caller may change this to an actual number.
862
863=item B<< $item->version >> (read-only)
864
865The version number for this item.  Please note that these version numbers
866have underscore (C<_>) as a separator for the version parts.
867
868=item B<< $item->exists >> (read-only)
869
870A boolean that tells if this symbol exists in code or not.
871
872=item B<< $item->platforms >> (read-only)
873
874A hash table reference.  The keys of the hash table are the names of
875the specified platforms, with a value of 0 to indicate that this symbol
876isn't available on that platform, and 1 to indicate that it is.  Platforms
877that aren't mentioned default to 1.
878
879=item B<< $item->type >> (read-only)
880
881C<FUNCTION> or C<VARIABLE>, depending on what the symbol represents.
882Some platforms do not care about this, others do.
883
884=item B<< $item->features >> (read-only)
885
886An array reference, where every item indicates a feature where this symbol
887is available.  If no features are mentioned, the symbol is always available.
888If any feature is mentioned, this symbol is I<only> available when those
889features are enabled.
890
891=cut
892
893our $AUTOLOAD;
894
895# Generic getter
896sub AUTOLOAD {
897    my $self = shift;
898    my $funcname = $AUTOLOAD;
899    (my $item = $funcname) =~ s|.*::||g;
900
901    croak "$funcname called as setter" if @_;
902    croak "$funcname invalid" unless exists $self->{$item};
903    return $self->{$item} if ref($self->{$item}) eq '';
904    return @{$self->{$item}} if ref($self->{$item}) eq 'ARRAY';
905    return %{$self->{$item}} if ref($self->{$item}) eq 'HASH';
906}
907
908=item B<< $item->intnum >> (read-write)
909
910Internal positional number.  If I<< $item->number >> is '?' or '?+', the
911caller can use this to set a number for its purposes.
912If I<< $item->number >> is a number, I<< $item->intnum >> should be the
913same
914
915=cut
916
917# Getter/setters
918sub intnum {
919    my $self = shift;
920    my $value = shift;
921    my $item = 'intnum';
922
923    croak "$item called with extra arguments" if @_;
924    $self->{$item} = "$value" if defined $value;
925    return $self->{$item};
926}
927
928sub number {
929    my $self = shift;
930    my $value = shift;
931    my $item = 'number';
932
933    croak "$item called with extra arguments" if @_;
934    $self->{$item} = "$value" if defined $value;
935    return $self->{$item};
936}
937
938=item B<< $item->to_string >>
939
940Converts the item to a string that can be saved in an ordinals file.
941
942=cut
943
944sub to_string {
945    my $self = shift;
946
947    croak "Too many arguments" if @_;
948    my %platforms = $self->platforms();
949    my @features = $self->features();
950    my $version = $self->version();
951    $version =~ s|\.|_|g;
952    return sprintf "%-39s %s\t%s\t%s:%s:%s:%s",
953        $self->name(),
954        $self->number(),
955        $version,
956        $self->exists() ? 'EXIST' : 'NOEXIST',
957        join(',', (map { ($platforms{$_} ? '' : '!') . $_ }
958                   sort keys %platforms)),
959        $self->type(),
960        join(',', @features);
961}
962
963=back
964
965=head2 Comparators and filters
966
967For the B<< $ordinals->items >> method, there are a few functions to create
968comparators based on specific data:
969
970=over 4
971
972=cut
973
974# Go back to the main package to create comparators and filters
975package OpenSSL::Ordinals;
976
977# Comparators...
978
979=item B<by_name>
980
981Returns a comparator that will compare the names of two OpenSSL::Ordinals::Item
982objects.
983
984=cut
985
986sub by_name {
987    return sub { $_[0]->name() cmp $_[1]->name() };
988}
989
990=item B<by_number>
991
992Returns a comparator that will compare the ordinal numbers of two
993OpenSSL::Ordinals::Item objects.
994
995=cut
996
997sub by_number {
998    return sub { $_[0]->intnum() <=> $_[1]->intnum() };
999}
1000
1001=item B<by_version>
1002
1003Returns a comparator that will compare the version of two
1004OpenSSL::Ordinals::Item objects.
1005
1006=cut
1007
1008sub by_version {
1009    return sub {
1010        # cmp_versions comes from OpenSSL::Util
1011        return cmp_versions($_[0]->version(), $_[1]->version());
1012    }
1013}
1014
1015=back
1016
1017There are also the following filters:
1018
1019=over 4
1020
1021=cut
1022
1023# Filters...  these are called by grep, the return sub must use $_ for
1024# the item to check
1025
1026=item B<f_version VERSION>
1027
1028Returns a filter that only lets through symbols with a version number
1029matching B<VERSION>.
1030
1031=cut
1032
1033sub f_version {
1034    my $version = shift;
1035
1036    croak "No version specified"
1037        unless $version && $version =~ /^\d+\.\d+\.\d+[a-z]{0,2}$/;
1038
1039    return sub { $_[0]->version() eq $version };
1040}
1041
1042=item B<f_number NUMBER>
1043
1044Returns a filter that only lets through symbols with the ordinal number
1045matching B<NUMBER>.
1046
1047NOTE that this returns a "magic" value that can not be used as a function.
1048It's only useful when passed directly as a filter to B<items>.
1049
1050=cut
1051
1052sub f_number {
1053    my $number = shift;
1054
1055    croak "No number specified"
1056        unless $number && $number =~ /^\d+$/;
1057
1058    return [ F_NUMBER, $number ];
1059}
1060
1061
1062=item B<f_name NAME>
1063
1064Returns a filter that only lets through symbols with the symbol name
1065matching B<NAME>.
1066
1067NOTE that this returns a "magic" value that can not be used as a function.
1068It's only useful when passed directly as a filter to B<items>.
1069
1070=cut
1071
1072sub f_name {
1073    my $name = shift;
1074
1075    croak "No name specified"
1076        unless $name;
1077
1078    return [ F_NAME, $name ];
1079}
1080
1081=back
1082
1083=head1 AUTHORS
1084
1085Richard Levitte E<lt>levitte@openssl.orgE<gt>.
1086
1087=cut
1088
10891;
1090