summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/Dist/Metadata.pm677
-rw-r--r--lib/Dist/Metadata/Archive.pm171
-rw-r--r--lib/Dist/Metadata/Dir.pm186
-rw-r--r--lib/Dist/Metadata/Dist.pm623
-rw-r--r--lib/Dist/Metadata/Struct.pm131
-rw-r--r--lib/Dist/Metadata/Tar.pm99
-rw-r--r--lib/Dist/Metadata/Zip.pm95
7 files changed, 1982 insertions, 0 deletions
diff --git a/lib/Dist/Metadata.pm b/lib/Dist/Metadata.pm
new file mode 100644
index 0000000..a05414e
--- /dev/null
+++ b/lib/Dist/Metadata.pm
@@ -0,0 +1,677 @@
+# vim: set ts=2 sts=2 sw=2 expandtab smarttab:
+#
+# This file is part of Dist-Metadata
+#
+# This software is copyright (c) 2011 by Randy Stauner.
+#
+# This is free software; you can redistribute it and/or modify it under
+# the same terms as the Perl 5 programming language system itself.
+#
+use strict;
+use warnings;
+
+package Dist::Metadata;
+# git description: v0.925-17-g08a6891
+
+our $AUTHORITY = 'cpan:RWSTAUNER';
+# ABSTRACT: Information about a perl module distribution
+$Dist::Metadata::VERSION = '0.926';
+use Carp qw(croak carp);
+use CPAN::Meta 2.1 ();
+use List::Util qw(first); # core in perl v5.7.3
+
+# something that is obviously not a real value
+sub UNKNOWN () { '- unknown -' } # constant
+
+
+sub new {
+ my $class = shift;
+ my $self = {
+ determine_packages => 1,
+ @_ == 1 ? %{ $_[0] } : @_
+ };
+
+ my @formats = qw( dist file dir struct );
+ croak(qq[A dist must be specified (one of ] .
+ join(', ', map { "'$_'" } @formats) . ')')
+ unless first { $self->{$_} } @formats;
+
+ bless $self, $class;
+}
+
+
+sub dist {
+ my ($self) = @_;
+ return $self->{dist} ||= do {
+ my $dist;
+ if( my $struct = $self->{struct} ){
+ require Dist::Metadata::Struct;
+ $dist = Dist::Metadata::Struct->new(%$struct);
+ }
+ elsif( my $dir = $self->{dir} ){
+ require Dist::Metadata::Dir;
+ $dist = Dist::Metadata::Dir->new(dir => $dir);
+ }
+ elsif ( my $file = $self->{file} ){
+ require Dist::Metadata::Archive;
+ $dist = Dist::Metadata::Archive->new(file => $file);
+ }
+ else {
+ # new() checks for one and dies without so we shouldn't get here
+ croak q[No dist format parameters found!];
+ }
+ $dist; # return
+ };
+}
+
+
+sub default_metadata {
+ my ($self) = @_;
+
+ return {
+ # required
+ abstract => UNKNOWN,
+ author => [],
+ dynamic_config => 0,
+ generated_by => ( ref($self) || $self ) . ' version ' . ( $self->VERSION || 0 ),
+ license => ['unknown'], # this 'unknown' comes from CPAN::Meta::Spec
+ 'meta-spec' => {
+ version => '2',
+ url => 'http://search.cpan.org/perldoc?CPAN::Meta::Spec',
+ },
+ name => UNKNOWN,
+
+ # strictly speaking, release_status is also required but
+ # CPAN::Meta will figure it out based on the version number. if
+ # we were to set it explicitly, then we would first need to
+ # examine the version number for '_' or 'TRIAL' or 'RC' etc.
+
+ version => 0,
+
+ # optional
+ no_index => {
+ # Ignore the same directories as PAUSE (https://github.com/andk/pause/blob/master/lib/PAUSE/dist.pm#L758):
+ # skip "t" - libraries in ./t are test libraries!
+ # skip "xt" - libraries in ./xt are author test libraries!
+ # skip "inc" - libraries in ./inc are usually install libraries
+ # skip "local" - somebody shipped his carton setup!
+ # skip 'perl5" - somebody shipped her local::lib!
+ # skip 'fatlib' - somebody shipped their fatpack lib!
+ directory => [qw( inc t xt local perl5 fatlib )],
+ },
+ # provides => { package => { file => $file, version => $version } }
+ };
+}
+
+
+sub determine_metadata {
+ my ($self) = @_;
+
+ my $dist = $self->dist;
+ my $meta = $self->default_metadata;
+
+ # get name and version from dist if dist was able to parse them
+ foreach my $att (qw(name version)) {
+ my $val = $dist->$att;
+ # if the dist could determine it that's better than the default
+ # but undef won't validate. value in $self will still override.
+ $meta->{$att} = $val
+ if defined $val;
+ }
+
+ # any passed in values should take priority
+ foreach my $field ( keys %$meta ){
+ $meta->{$field} = $self->{$field}
+ if exists $self->{$field};
+ }
+
+ return $meta;
+}
+
+
+sub determine_packages {
+ # meta must be passed to avoid infinite loop
+ my ( $self, $meta ) = @_;
+ # if not passed in, use defaults (we just want the 'no_index' property)
+ $meta ||= $self->meta_from_struct( $self->determine_metadata );
+
+ # should_index_file() expects unix paths
+ my @files = grep {
+ $meta->should_index_file(
+ $self->dist->path_classify_file($_)->as_foreign('Unix')->stringify
+ );
+ }
+ $self->dist->perl_files;
+
+ # TODO: should we limit packages to lib/ if it exists?
+ # my @lib = grep { m#^lib/# } @files; @files = @lib if @lib;
+
+ return {} if not @files;
+
+ my $packages = $self->dist->determine_packages(@files);
+
+
+ foreach my $pack ( keys %$packages ) {
+
+ # Remove any packages that should not be indexed
+ if ( !$meta->should_index_package($pack) ) {
+ delete $packages->{$pack};
+ next;
+ }
+
+ unless( $self->{include_inner_packages} ){
+ # PAUSE only considers packages that match the basename of the
+ # containing file. For example, file Foo.pm may only contain a
+ # package that matches /\bFoo$/. This is what PAUSE calls a
+ # "simile". All other packages in the file will be ignored.
+
+ # capture file basename (without the extension)
+ my ($base) = ($packages->{$pack}->{file} =~ m!([^/]+)\.pm(?:\.PL)?$!);
+ # remove if file didn't match regexp or package doesn't match basename
+ delete $packages->{$pack}
+ if !$base || $pack !~ m{\b\Q$base\E$};
+ }
+ }
+
+ return $packages;
+}
+
+
+sub load_meta {
+ my ($self) = @_;
+
+ my $dist = $self->dist;
+ my @files = $dist->list_files;
+ my ( $meta, $metafile );
+ my $default_meta = $self->determine_metadata;
+
+ # prefer json file (spec v2)
+ if ( $metafile = first { m#^META\.json$# } @files ) {
+ $meta = CPAN::Meta->load_json_string( $dist->file_content($metafile) );
+ }
+ # fall back to yaml file (spec v1)
+ elsif ( $metafile = first { m#^META\.ya?ml$# } @files ) {
+ $meta = CPAN::Meta->load_yaml_string( $dist->file_content($metafile) );
+ }
+ # no META file found in dist
+ else {
+ $meta = $self->meta_from_struct( $default_meta );
+ }
+
+ {
+ # always include (never index) the default no_index dirs
+ my $dir = ($meta->{no_index} ||= {})->{directory} ||= [];
+ my %seen = map { ($_ => 1) } @$dir;
+ unshift @$dir,
+ grep { !$seen{$_}++ }
+ @{ $default_meta->{no_index}->{directory} };
+ }
+
+ # Something has to be indexed, so if META has no (or empty) 'provides'
+ # attempt to determine packages unless specifically configured not to
+ if ( !keys %{ $meta->provides || {} } && $self->{determine_packages} ) {
+ # respect api/encapsulation
+ my $struct = $meta->as_struct;
+ $struct->{provides} = $self->determine_packages($meta);
+ $meta = $self->meta_from_struct($struct);
+ }
+
+ return $meta;
+}
+
+
+sub meta {
+ my ($self) = @_;
+ return $self->{meta} ||= $self->load_meta;
+}
+
+
+sub meta_from_struct {
+ my ($self, $struct) = @_;
+ return CPAN::Meta->create( $struct, { lazy_validation => 1 } );
+}
+
+
+sub package_versions {
+ my ($self) = shift;
+ my $provides = @_ ? shift : $self->provides; # || {}
+ return {
+ map { ($_ => $provides->{$_}{version}) } keys %$provides
+ };
+}
+
+
+sub module_info {
+ my ($self, $opts) = @_;
+ my $provides = $opts->{provides} || $self->provides;
+ $provides = { %$provides }; # break reference
+
+ my $checksums = $opts->{checksum} || $opts->{digest} || [];
+ $checksums = [ $checksums ]
+ unless ref($checksums) eq 'ARRAY';
+
+ my $digest_cache = {};
+ foreach my $mod ( keys %$provides ){
+ my $data = { %{ $provides->{ $mod } } }; # break reference
+
+ foreach my $checksum ( @$checksums ){
+ $data->{ $checksum } =
+ $digest_cache->{ $data->{file} }->{ $checksum } ||=
+ $self->dist->file_checksum($data->{file}, $checksum);
+ }
+
+ # TODO: $opts->{callback}->($self, $mod, $data, sub { $self->dist->file_content($data->{file}) });
+
+ $provides->{ $mod } = $data;
+ }
+
+ return $provides;
+}
+
+
+{
+ no strict 'refs'; ## no critic (NoStrict)
+ foreach my $method ( qw(
+ name
+ provides
+ version
+ ) ){
+ *$method = sub { $_[0]->meta->$method };
+ }
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=for :stopwords Randy Stauner ACKNOWLEDGEMENTS TODO dist dists dir unix checksum checksums
+David Jeffrey Ryan Sawyer Steinbrunner Thalhammer X cpan testmatrix url
+annocpan anno bugtracker rt cpants kwalitee diff irc mailto metadata
+placeholders metacpan
+
+=head1 NAME
+
+Dist::Metadata - Information about a perl module distribution
+
+=head1 VERSION
+
+version 0.926
+
+=head1 SYNOPSIS
+
+ my $dist = Dist::Metadata->new(file => $path_to_archive);
+
+ my $description = sprintf "Dist %s (%s)", $dist->name, $dist->version;
+
+ my $provides = $dist->package_versions;
+ while( my ($package, $version) = each %$provides ){
+ print "$description includes $package $version\n";
+ }
+
+=head1 DESCRIPTION
+
+This module provides an easy interface for getting various metadata
+about a Perl module distribution.
+
+It takes care of the common logic of:
+
+=over 4
+
+=item *
+
+reading a tar file (L<Archive::Tar>)
+
+=item *
+
+finding and reading the correct META file if the distribution contains one (L<CPAN::Meta>)
+
+=item *
+
+and determining some of the metadata if there is no META file (L<Module::Metadata>, L<CPAN::DistnameInfo>)
+
+=back
+
+This is mostly a wrapper around L<CPAN::Meta> providing an easy interface
+to find and load the meta file from a F<tar.gz> file.
+A dist can also be represented by a directory or merely a structure of data.
+
+If the dist does not contain a meta file
+the module will attempt to determine some of that data from the dist.
+
+B<NOTE>: This interface is still being defined.
+Please submit any suggestions or concerns.
+
+=head1 METHODS
+
+=head2 new
+
+ Dist::Metadata->new(file => $path);
+
+A dist can be represented by
+a tar file,
+a directory,
+or a data structure.
+
+The format will be determined by the presence of the following options
+(checked in this order):
+
+=over 4
+
+=item *
+
+C<struct> - hash of data to build a mock dist; See L<Dist::Metadata::Struct>.
+
+=item *
+
+C<dir> - path to the root directory of a dist
+
+=item *
+
+C<file> - the path to a F<.tar.gz> file
+
+=back
+
+You can also slyly pass in your own object as a C<dist> parameter
+in which case this module will just use that.
+This can be useful if you need to use your own subclass
+(perhaps while developing a new format).
+
+Other options that can be specified:
+
+=over 4
+
+=item *
+
+C<name> - dist name
+
+=item *
+
+C<version> - dist version
+
+=item *
+
+C<determine_packages> - boolean to indicate whether dist should be searched
+for packages if no META file is found. Defaults to true.
+
+=item *
+
+C<include_inner_packages> - When determining provided packages
+the default behavior is to only include packages that match the name
+of the file that defines them (like C<Foo::Bar> matches C<*/Bar.pm>).
+This way only modules that can be loaded (via C<use> or C<require>)
+will be returned (and "inner" packages will be ignored).
+This mimics the behavior of PAUSE.
+Set this to true to include any "inner" packages provided by the dist
+(that are not otherwise excluded by another mechanism (such as C<no_index>)).
+
+=back
+
+=head2 dist
+
+Returns the dist object (subclass of L<Dist::Metadata::Dist>).
+
+=head2 default_metadata
+
+Returns a hashref of default values
+used to initialize a L<CPAN::Meta> object
+when a META file is not found.
+Called from L</determine_metadata>.
+
+=head2 determine_metadata
+
+Examine the dist and try to determine metadata.
+Returns a hashref which can be passed to L<CPAN::Meta/new>.
+This is used when the dist does not contain a META file.
+
+=head2 determine_packages
+
+ my $provides = $dm->determine_packages($meta);
+
+Attempt to determine packages provided by the dist.
+This is used when the META file does not include a C<provides>
+section and C<determine_packages> is not set to false in the constructor.
+
+If a L<CPAN::Meta> object is not provided a default one will be used.
+Files contained in the dist and packages found therein will be checked against
+the meta object's C<no_index> attribute
+(see L<CPAN::Meta/should_index_file>
+and L<CPAN::Meta/should_index_package>).
+By default this ignores any files found in
+F<inc/>,
+F<t/>,
+or F<xt/>
+directories.
+
+=head2 load_meta
+
+Loads the metadata from the L</dist>.
+
+=head2 meta
+
+Returns the L<CPAN::Meta> instance in use.
+
+=head2 meta_from_struct
+
+ $meta = $dm->meta_from_struct(\%struct);
+
+Passes the provided C<\%struct> to L<CPAN::Meta/create>
+and returns the result.
+
+=head2 package_versions
+
+ $pv = $dm->package_versions();
+ # { 'Package::Name' => '1.0', 'Module::2' => '2.1' }
+
+Returns a simplified version of C<provides>:
+a hashref with package names as keys and versions as values.
+
+This can also be called as a class method
+which will operate on a passed in hashref.
+
+ $pv = Dist::Metadata->package_versions(\%provides);
+
+=head2 module_info
+
+Returns a hashref of meta data for each of the packages provided by this dist.
+
+The hashref starts with the same data as L</provides>
+but additional data can be added to the output by specifying options in a hashref:
+
+=over 4
+
+=item C<checksum>
+
+Use the specified algorithm to compute a hex digest of the file.
+The type you specify will be the key in the returned hashref.
+You can use an arrayref to specify more than one type.
+
+ $dm->module_info({checksum => ['sha256', 'md5']});
+ # returns:
+ {
+ 'Mod::Name' => {
+ file => 'lib/Mod/Name.pm',
+ version => '0.1',
+ md5 => '258e88dcbd3cd44d8e7ab43f6ecb6af0',
+ sha256 => 'f22136124cd3e1d65a48487cecf310771b2fd1e83dc032e3d19724160ac0ff71',
+ },
+ }
+
+See L<Dist::Metadata::Dist/file_checksum> for more information.
+
+=item C<provides>
+
+The default is to start with the hashref returned from L</provides>
+but you can pass in an alternate hashref using this key.
+
+=back
+
+Other options may be added in the future.
+
+=head1 INHERITED METHODS
+
+The following methods are available on this object
+and simply call the corresponding method on the L<CPAN::Meta> object.
+
+=over 4
+
+=item *
+
+X<name> name
+
+=item *
+
+X<provides> provides
+
+=item *
+
+X<version> version
+
+=back
+
+=for Pod::Coverage name version provides
+UNKNOWN
+
+=for test_synopsis my $path_to_archive;
+
+=head1 TODO
+
+=over 4
+
+=item *
+
+More tests
+
+=item *
+
+C<trust_meta> option (to allow setting it to false)
+
+=item *
+
+Guess main module from dist name if no packages can be found
+
+=item *
+
+Determine abstract?
+
+=item *
+
+Add change log info (L<CPAN::Changes>)?
+
+=item *
+
+Subclass as C<CPAN::Dist::Metadata> just so that it has C<CPAN> in the name?
+
+=item *
+
+Use L<File::Find::Rule::Perl>?
+
+=back
+
+=head1 SEE ALSO
+
+=head2 Dependencies
+
+=over 4
+
+=item *
+
+L<CPAN::Meta>
+
+=item *
+
+L<Module::Metadata>
+
+=item *
+
+L<CPAN::DistnameInfo>
+
+=back
+
+=head2 Related Modules
+
+=over 4
+
+=item *
+
+L<MyCPAN::Indexer>
+
+=item *
+
+L<CPAN::ParseDistribution>
+
+=back
+
+=head1 SUPPORT
+
+=head2 Perldoc
+
+You can find documentation for this module with the perldoc command.
+
+ perldoc Dist::Metadata
+
+=head2 Websites
+
+The following websites have more information about this module, and may be of help to you. As always,
+in addition to those websites please use your favorite search engine to discover more resources.
+
+=over 4
+
+=item *
+
+MetaCPAN
+
+A modern, open-source CPAN search engine, useful to view POD in HTML format.
+
+L<http://metacpan.org/release/Dist-Metadata>
+
+=back
+
+=head2 Bugs / Feature Requests
+
+Please report any bugs or feature requests by email to C<bug-dist-metadata at rt.cpan.org>, or through
+the web interface at L<https://rt.cpan.org/Public/Bug/Report.html?Queue=Dist-Metadata>. You will be automatically notified of any
+progress on the request by the system.
+
+=head2 Source Code
+
+
+L<https://github.com/rwstauner/Dist-Metadata>
+
+ git clone https://github.com/rwstauner/Dist-Metadata.git
+
+=head1 AUTHOR
+
+Randy Stauner <rwstauner@cpan.org>
+
+=head1 CONTRIBUTORS
+
+=for stopwords David Steinbrunner Jeffrey Ryan Thalhammer Sawyer X
+
+=over 4
+
+=item *
+
+David Steinbrunner <dsteinbrunner@pobox.com>
+
+=item *
+
+Jeffrey Ryan Thalhammer <thaljef@cpan.org>
+
+=item *
+
+Sawyer X <xsawyerx@cpan.org>
+
+=back
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2011 by Randy Stauner.
+
+This is free software; you can redistribute it and/or modify it under
+the same terms as the Perl 5 programming language system itself.
+
+=cut
diff --git a/lib/Dist/Metadata/Archive.pm b/lib/Dist/Metadata/Archive.pm
new file mode 100644
index 0000000..cb0bb60
--- /dev/null
+++ b/lib/Dist/Metadata/Archive.pm
@@ -0,0 +1,171 @@
+# vim: set ts=2 sts=2 sw=2 expandtab smarttab:
+#
+# This file is part of Dist-Metadata
+#
+# This software is copyright (c) 2011 by Randy Stauner.
+#
+# This is free software; you can redistribute it and/or modify it under
+# the same terms as the Perl 5 programming language system itself.
+#
+use strict;
+use warnings;
+
+package Dist::Metadata::Archive;
+our $AUTHORITY = 'cpan:RWSTAUNER';
+# ABSTRACT: Base class for Dist::Metadata archive files
+$Dist::Metadata::Archive::VERSION = '0.926';
+use Carp (); # core
+use parent 'Dist::Metadata::Dist';
+
+push(@Dist::Metadata::CARP_NOT, __PACKAGE__);
+
+
+sub new {
+ my $class = shift;
+ my $self = $class->SUPER::new(@_);
+
+ if( $class eq __PACKAGE__ ){
+ my $subclass = 'Dist::Metadata::' .
+ ( $self->{file} =~ /\.zip$/ ? 'Zip' : 'Tar' );
+
+ eval "require $subclass"
+ or Carp::croak $@;
+
+ # rebless into format specific subclass
+ bless $self, $subclass;
+ }
+
+ return $self;
+}
+
+sub required_attribute { 'file' }
+
+
+sub archive {
+ my ($self) = @_;
+ return $self->{archive} ||= do {
+ my $file = $self->file;
+
+ Carp::croak "File '$file' does not exist"
+ unless -e $file;
+
+ $self->read_archive($file); # return
+ };
+}
+
+
+sub default_file_spec { 'Unix' }
+
+
+sub determine_name_and_version {
+ my ($self) = @_;
+ $self->set_name_and_version( $self->parse_name_and_version( $self->file ) );
+ return $self->SUPER::determine_name_and_version(@_);
+}
+
+
+sub file {
+ return $_[0]->{file};
+}
+
+
+sub read_archive {
+ Carp::croak q[Method 'read_archive' not defined];
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=for :stopwords Randy Stauner ACKNOWLEDGEMENTS TODO dist dists dir unix checksum checksums
+David Jeffrey Ryan Sawyer Steinbrunner Thalhammer X
+
+=head1 NAME
+
+Dist::Metadata::Archive - Base class for Dist::Metadata archive files
+
+=head1 VERSION
+
+version 0.926
+
+=head1 SYNOPSIS
+
+ my $dist = Dist::Metadata->new(file => $path_to_archive);
+
+=head1 DESCRIPTION
+
+This is a subclass of L<Dist::Metadata::Dist>
+to enable determining the metadata from an archive file.
+
+It is a base class for archive file formats:
+
+=over 4
+
+=item *
+
+L<Dist::Metadata::Tar>
+
+=item *
+
+L<Dist::Metadata::Zip>
+
+=back
+
+It's not useful on it's own
+and should be used from L<Dist::Metadata/new>.
+
+=head1 METHODS
+
+=head2 new
+
+ $dist = Dist::Metadata::Archive->new(file => $path);
+
+Accepts a single C<file> argument that should be a path to a file.
+
+If called from this base class
+C<new()> will delegate to a subclass based on the filename
+and return a blessed instance of that subclass.
+
+=head2 archive
+
+Returns an object representing the archive file.
+
+=head2 default_file_spec
+
+Returns C<Unix> since most archive files are be in unix format.
+
+=head2 determine_name_and_version
+
+Attempts to parse name and version from file name.
+
+=head2 file
+
+The C<file> attribute passed to the constructor,
+used to load L</archive>.
+
+=head2 read_archive
+
+ $dist->read_archive($file);
+
+Returns a format-specific object representing the specified file.
+
+This B<must> be defined by subclasses.
+
+=for test_synopsis my $path_to_archive;
+
+=head1 AUTHOR
+
+Randy Stauner <rwstauner@cpan.org>
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2011 by Randy Stauner.
+
+This is free software; you can redistribute it and/or modify it under
+the same terms as the Perl 5 programming language system itself.
+
+=cut
diff --git a/lib/Dist/Metadata/Dir.pm b/lib/Dist/Metadata/Dir.pm
new file mode 100644
index 0000000..78812a2
--- /dev/null
+++ b/lib/Dist/Metadata/Dir.pm
@@ -0,0 +1,186 @@
+# vim: set ts=2 sts=2 sw=2 expandtab smarttab:
+#
+# This file is part of Dist-Metadata
+#
+# This software is copyright (c) 2011 by Randy Stauner.
+#
+# This is free software; you can redistribute it and/or modify it under
+# the same terms as the Perl 5 programming language system itself.
+#
+use strict;
+use warnings;
+
+package Dist::Metadata::Dir;
+our $AUTHORITY = 'cpan:RWSTAUNER';
+# ABSTRACT: Enable Dist::Metadata for a directory
+$Dist::Metadata::Dir::VERSION = '0.926';
+use Carp qw(croak carp); # core
+use File::Find (); # core
+use Path::Class 0.24 ();
+use parent 'Dist::Metadata::Dist';
+
+push(@Dist::Metadata::CARP_NOT, __PACKAGE__);
+
+
+sub new {
+ my $class = shift;
+ my $self = $class->SUPER::new(@_);
+
+ # fix up dir (for example chop trailing slash if present)
+ $self->{dir} = $self->path_class_dir->new($self->{dir})->stringify;
+
+ # TODO: croak if not -d $self->dir
+
+ return $self;
+}
+
+sub required_attribute { 'dir' }
+
+
+sub determine_name_and_version {
+ my ($self) = @_;
+ # 'root' may be more accurate than 'dir'
+ $self->SUPER::determine_name_and_version();
+ $self->set_name_and_version( $self->parse_name_and_version( $self->dir ) );
+ return;
+}
+
+
+sub dir {
+ $_[0]->{dir};
+}
+
+# this shouldn't be called
+sub extract_into {
+ croak q[A directory doesn't need to be extracted];
+}
+
+
+sub file_content {
+ my ($self, $file) = @_;
+ # This is a directory so file spec will always be native
+ my $path = $self->path_class_file
+ ->new( $self->{dir}, $self->full_path($file) )->stringify;
+
+ open(my $fh, '<', $path)
+ or croak "Failed to open file '$path': $!";
+
+ return do { local $/; <$fh> };
+}
+
+
+sub find_files {
+ my ($self) = @_;
+
+ my $dir = $self->{dir};
+ my @files;
+
+ File::Find::find(
+ {
+ wanted => sub {
+ push @files, $self->path_class_file->new($_)->relative($dir)->stringify
+ if -f $_;
+ },
+ no_chdir => 1
+ },
+ $dir
+ );
+
+ return @files;
+}
+
+
+sub physical_directory {
+ my ($self, @files) = @_;
+
+ # TODO: return absolute_path?
+ my @parts = $self->{dir};
+ # go into root dir if there is one
+ push @parts, $self->root
+ if $self->root;
+
+ my $dir = $self->path_class_dir->new(@parts)->absolute;
+
+ return $dir->stringify unless wantarray;
+
+ return map { $_->stringify }
+ ($dir, map { $dir->file( $_ ) } @files);
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=for :stopwords Randy Stauner ACKNOWLEDGEMENTS TODO dist dists dir unix checksum checksums
+David Jeffrey Ryan Sawyer Steinbrunner Thalhammer X
+
+=head1 NAME
+
+Dist::Metadata::Dir - Enable Dist::Metadata for a directory
+
+=head1 VERSION
+
+version 0.926
+
+=head1 SYNOPSIS
+
+ my $dm = Dist::Metadata->new(dir => $path_to_dir);
+
+=head1 DESCRIPTION
+
+This is a subclass of L<Dist::Metadata::Dist>
+to enable getting the dists metadata from a directory.
+
+This can be useful if you already have a dist extracted into a directory.
+
+It's probably not very useful on it's own though,
+and should be used from L<Dist::Metadata/new>.
+
+=head1 METHODS
+
+=head2 new
+
+ $dist = Dist::Metadata::Struct->new(dir => $path);
+
+Accepts a single 'dir' argument that should be a path to a directory.
+
+=head2 determine_name_and_version
+
+Attempts to parse name and version from directory name.
+
+=head2 dir
+
+Returns the C<dir> attribute specified in the constructor.
+
+=head2 file_content
+
+Returns the content for the specified file.
+
+=head2 find_files
+
+Returns a list of the file names beneath the directory
+(relative to the directory).
+
+=head2 physical_directory
+
+Returns the C<dir> attribute since this is already a directory
+containing the desired files.
+
+=for test_synopsis my $path_to_dir;
+
+=head1 AUTHOR
+
+Randy Stauner <rwstauner@cpan.org>
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2011 by Randy Stauner.
+
+This is free software; you can redistribute it and/or modify it under
+the same terms as the Perl 5 programming language system itself.
+
+=cut
diff --git a/lib/Dist/Metadata/Dist.pm b/lib/Dist/Metadata/Dist.pm
new file mode 100644
index 0000000..063affd
--- /dev/null
+++ b/lib/Dist/Metadata/Dist.pm
@@ -0,0 +1,623 @@
+#
+# This file is part of Dist-Metadata
+#
+# This software is copyright (c) 2011 by Randy Stauner.
+#
+# This is free software; you can redistribute it and/or modify it under
+# the same terms as the Perl 5 programming language system itself.
+#
+use strict;
+use warnings;
+
+package Dist::Metadata::Dist;
+our $AUTHORITY = 'cpan:RWSTAUNER';
+# ABSTRACT: Base class for format-specific implementations
+$Dist::Metadata::Dist::VERSION = '0.926';
+use Carp qw(croak carp); # core
+use CPAN::DistnameInfo 0.12 ();
+use Path::Class 0.24 ();
+use Try::Tiny 0.09;
+
+
+sub new {
+ my $class = shift;
+ my $self = {
+ @_ == 1 ? %{ $_[0] } : @_
+ };
+
+ bless $self, $class;
+
+ my $req = $class->required_attribute;
+ croak qq['$req' parameter required]
+ if $req && !$self->{$req};
+
+ if ( exists $self->{file_spec} ) {
+ # we just want the OS name ('Unix' or '')
+ $self->{file_spec} =~ s/^File::Spec(::)?//
+ if $self->{file_spec};
+ # blank is no good, use "Native" hack
+ $self->{file_spec} = 'Native'
+ if !$self->{file_spec};
+ }
+
+ return $self;
+}
+
+
+sub default_file_spec { 'Native' }
+
+
+sub determine_name_and_version {
+ my ($self) = @_;
+ $self->set_name_and_version( $self->parse_name_and_version( $self->root ) );
+ return;
+}
+
+
+sub determine_packages {
+ my ($self, @files) = @_;
+
+ my $determined = try {
+ my @dir_and_files = $self->physical_directory(@files);
+
+ # return
+ $self->packages_from_directory(@dir_and_files);
+ }
+ catch {
+ carp("Error determining packages: $_[0]");
+ +{}; # return
+ };
+
+ return $determined;
+}
+
+
+sub extract_into {
+ my ($self, $dir, @files) = @_;
+
+ @files = $self->list_files
+ unless @files;
+
+ require File::Basename;
+
+ my @disk_files;
+ foreach my $file (@files) {
+ my $ff = $self->path_class_file->new_foreign( $self->file_spec, $file );
+ # Translate dist format (relative path) to disk/OS format and prepend $dir.
+ # This dir_list + basename hack is probably ok because the paths in a dist
+ # should always be relative (if there *was* a volume we wouldn't want it).
+ my $path = $self->path_class_file
+ ->new( $dir, $ff->dir->dir_list, $ff->basename );
+
+ $path->dir->mkpath(0, oct(700));
+
+ my $full_path = $path->stringify;
+ open(my $fh, '>', $full_path)
+ or croak "Failed to open '$full_path' for writing: $!";
+ print $fh $self->file_content($file);
+
+ # do we really want full path or do we want relative?
+ push(@disk_files, $full_path);
+ }
+
+ return (wantarray ? ($dir, @disk_files) : $dir);
+}
+
+
+sub file_content {
+ croak q[Method 'file_content' not defined];
+}
+
+
+sub file_checksum {
+ my ($self, $file, $type) = @_;
+ $type ||= 'md5';
+
+ require Digest; # core
+
+ # md5 => MD5, sha256 => SHA-256
+ (my $impl = uc $type) =~ s/^(SHA|CRC)([0-9]+)$/$1-$2/;
+
+ my $digest = Digest->new($impl);
+
+ $digest->add( $self->file_content($file) );
+ return $digest->hexdigest;
+}
+
+
+sub find_files {
+ croak q[Method 'find_files' not defined];
+}
+
+
+sub file_spec {
+ my ($self) = @_;
+
+ $self->{file_spec} = $self->default_file_spec
+ if !exists $self->{file_spec};
+
+ return $self->{file_spec};
+}
+
+
+sub full_path {
+ my ($self, $file) = @_;
+
+ return $file
+ unless my $root = $self->root;
+
+ # don't re-add the root if it's already there
+ return $file
+ # FIXME: this regexp is probably not cross-platform...
+ # FIXME: is there a way to do this with File::Spec?
+ if $file =~ m@^\Q${root}\E[\\/]@;
+
+ # FIXME: does this foreign_file work w/ Dir ?
+ return $self->path_class_file
+ ->new_foreign($self->file_spec, $root, $file)->stringify;
+}
+
+
+sub list_files {
+ my ($self) = @_;
+
+ $self->{_list_files} = do {
+ my @files = sort $self->find_files;
+ my ($root, @rel) = $self->remove_root_dir(@files);
+ $self->{root} = $root;
+ \@rel; # return
+ }
+ unless $self->{_list_files};
+
+ return @{ $self->{_list_files} };
+}
+
+
+{
+ no strict 'refs'; ## no critic (NoStrict)
+ foreach my $method ( qw(
+ name
+ version
+ ) ){
+ *$method = sub {
+ my ($self) = @_;
+
+ $self->determine_name_and_version
+ if !exists $self->{ $method };
+
+ return $self->{ $method };
+ };
+ }
+}
+
+
+sub packages_from_directory {
+ my ($self, $dir, @files) = @_;
+
+ my @pvfd = ($dir);
+ # M::M::p_v_f_d expects full paths for \@files
+ push @pvfd, [map {
+ $self->path_class_file->new($_)->is_absolute
+ ? $_ : $self->path_class_file->new($dir, $_)->stringify
+ } @files]
+ if @files;
+
+ require Module::Metadata;
+
+ my $provides = try {
+ my $packages = Module::Metadata->package_versions_from_directory(@pvfd);
+ while ( my ($pack, $pv) = each %$packages ) {
+ # M::M::p_v_f_d returns files in native OS format (obviously);
+ # CPAN::Meta expects file paths in Unix format
+ $pv->{file} = $self->path_class_file
+ ->new($pv->{file})->as_foreign('Unix')->stringify;
+ }
+ $packages; # return
+ }
+ catch {
+ carp("Failed to determine packages: $_[0]");
+ +{}; # return
+ };
+ return $provides || {};
+}
+
+
+sub parse_name_and_version {
+ my ($self, $path) = @_;
+ my ( $name, $version );
+ if ( $path ){
+ # try a simple regexp first
+ $path =~ m!
+ ([^\\/]+) # name (anything below final directory)
+ - # separator
+ (v?[0-9._]+) # version
+ (?: # possible file extensions
+ \.t(?:ar\.)?gz
+ )?
+ $
+ !x and
+ ( $name, $version ) = ( $1, $2 );
+
+ # attempt to improve data with CPAN::DistnameInfo (but ignore any errors)
+ # TODO: also grab maturity and cpanid ?
+ # release_status = $dist->maturity eq 'released' ? 'stable' : 'unstable';
+ # -(TRIAL|RC) => 'testing', '_' => 'unstable'
+ eval {
+ # DistnameInfo expects any directories in unix format (thanks jeroenl)
+ my $dnifile = $self->path_class_file
+ ->new($path)->as_foreign('Unix')->stringify;
+ # if it doesn't appear to have an extension fake one to help DistnameInfo
+ $dnifile .= '.tar.gz' unless $dnifile =~ /\.[a-z]\w+$/;
+
+ my $dni = CPAN::DistnameInfo->new($dnifile);
+ my $dni_name = $dni->dist;
+ my $dni_version = $dni->version;
+ # if dni matched both name and version, or previous regexp didn't match
+ if ( $dni_name && $dni_version || !$name ) {
+ $name = $dni_name if $dni_name;
+ $version = $dni_version if $dni_version;
+ }
+ };
+ warn $@ if $@;
+ }
+ return ($name, $version);
+}
+
+
+sub path_class_dir { $_[0]->{path_class_dir} ||= 'Path::Class::Dir' }
+sub path_class_file { $_[0]->{path_class_file} ||= 'Path::Class::File' }
+
+
+sub path_classify_dir {
+ my ($self, $dir) = @_;
+ $self->path_class_dir->new_foreign($self->file_spec, $dir)
+}
+
+sub path_classify_file {
+ my ($self, $file) = @_;
+ $self->path_class_file->new_foreign($self->file_spec, $file)
+}
+
+
+sub perl_files {
+ return
+ grep { /\.pm$/ }
+ $_[0]->list_files;
+}
+
+
+sub physical_directory {
+ my ($self, @files) = @_;
+
+ require File::Temp;
+ # dir will be removed when return value goes out of scope (in caller)
+ my $dir = File::Temp->newdir();
+
+ return $self->extract_into($dir, @files);
+}
+
+
+sub remove_root_dir {
+ my ($self, @files) = @_;
+ return unless @files;
+
+ # FIXME: can we use File::Spec for these regexp's instead of [\\/] ?
+
+ # grab the root dir from the first file
+ $files[0] =~ m{^([^\\/]+)[\\/]}
+ # if not matched quit now
+ or return (undef, @files);
+
+ my $dir = $1;
+ my @rel;
+
+ # strip $dir from each file
+ for (@files) {
+
+ m{^\Q$dir\E[\\/](.+)$}
+ # if the match failed they're not all under the same root so just return now
+ or return (undef, @files);
+
+ push @rel, $1;
+ }
+
+ return ($dir, @rel);
+
+}
+
+
+sub required_attribute { return }
+
+
+sub root {
+ my ($self) = @_;
+
+ # call list_files instead of find_files so that it caches the result
+ $self->list_files
+ unless exists $self->{root};
+
+ return $self->{root};
+}
+
+
+sub set_name_and_version {
+ my ($self, @values) = @_;
+ my @fields = qw( name version );
+
+ foreach my $i ( 0 .. $#fields ){
+ $self->{ $fields[$i] } = $values[$i]
+ if !exists $self->{ $fields[$i] } && defined $values[$i];
+ }
+ return;
+}
+
+
+# version() defined with name()
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=for :stopwords Randy Stauner ACKNOWLEDGEMENTS TODO dist dists dir unix checksum checksums
+David Jeffrey Ryan Sawyer Steinbrunner Thalhammer X
+
+=head1 NAME
+
+Dist::Metadata::Dist - Base class for format-specific implementations
+
+=head1 VERSION
+
+version 0.926
+
+=head1 SYNOPSIS
+
+ # don't use this, use a subclass
+
+=head1 DESCRIPTION
+
+This is a base class for different dist formats.
+
+The following methods B<must> be defined by subclasses:
+
+=over 4
+
+=item *
+
+L</file_content>
+
+=item *
+
+L</find_files>
+
+=back
+
+=head1 METHODS
+
+=head2 new
+
+Simple constructor that subclasses can inherit.
+Ensures the presence of L</required_attribute>
+if defined by the subclass.
+
+=head2 default_file_spec
+
+Defaults to C<'Native'> in the base class
+which will let L<File::Spec> determine the value.
+
+=head2 determine_name_and_version
+
+Some dist formats may define a way to determine the name and version.
+
+=head2 determine_packages
+
+ $packages = $dist->determine_packages(@files);
+
+Search the specified files (or all files if unspecified)
+for perl packages.
+
+Extracts the files to a temporary directory if necessary
+and uses L<Module::Metadata> to discover package names and versions.
+
+=head2 extract_into
+
+ $ddir = $dist->extract_into($dir);
+ ($ddir, @dfiles) = $dist->extract_into($dir, @files);
+
+Extracts the specified files (or all files if not specified)
+into the specified directory.
+
+In list context this returns a list of the directory
+(which may be a subdirectory of the C<$dir> passed in)
+and the files extracted (in native OS (on-disk) format).
+
+In scalar context just the directory is returned.
+
+=head2 file_content
+
+Returns the content for the specified file from the dist.
+
+This B<must> be defined by subclasses.
+
+=head2 file_checksum
+
+ $dist->file_checksum('lib/Mod/Name.pm', 'sha256');
+
+Returns a checksum (hex digest) of the file content.
+
+The L<Digest> module is used to generate the checksums.
+The value specified should be one accepted by C<< Digest->new >>.
+A small effort is made to translate simpler names like
+C<md5> into C<MD5> and C<sha1> into C<SHA-1>
+(which are the names L<Digest> expects).
+
+If the type of checksum is not specified C<md5> will be used.
+
+=head2 find_files
+
+Determine the files contained in the dist.
+
+This is called from L</list_files> and cached on the object.
+
+This B<must> be defined by subclasses.
+
+=head2 file_spec
+
+Returns the OS name of the L<File::Spec> module used for this format.
+This is mostly so subclasses can define a specific one
+(as L</default_file_spec>) if necessary.
+
+A C<file_spec> attribute can be passed to the constructor
+to override the default.
+
+B<NOTE>: This is used for the internal format of the dist.
+Tar archives, for example, are always in unix format.
+For operations outside of the dist,
+the format determined by L<File::Spec> will always be used.
+
+=head2 full_path
+
+ $dist->full_path("lib/Mod.pm"); # "root-dir/lib/Mod.pm"
+
+Used internally to put the L</root> directory back onto the file.
+
+=head2 list_files
+
+Returns a list of the files in the dist starting at the dist root.
+
+This calls L</find_files> to get a listing of the contents of the dist,
+determines (and caches) the root directory (if any),
+caches and returns the list of files with the root dir stripped.
+
+ @files = $dist->list_files;
+ # something like qw( README META.yml lib/Mod.pm )
+
+=head2 name
+
+The dist name if it could be determined.
+
+=head2 packages_from_directory
+
+ $provides = $dist->packages_from_directory($dir, @files);
+
+Determines the packages provided by the perl modules found in a directory.
+This is thin wrapper around
+L<Module::Metadata/package_versions_from_directory>.
+It returns a hashref like L<CPAN::Meta::Spec/provides>.
+
+B<NOTE>: C<$dir> must be a physical directory on the disk,
+therefore C<@files> (if specified) must be in native OS format.
+This function is called internally from L</determine_packages>
+(which calls L<physical_directory> (which calls L</extract_into>))
+which manages these requirements.
+
+=head2 parse_name_and_version
+
+ ($name, $version) = $dist->parse_name_and_version($path);
+
+Attempt to parse name and version from the provided string.
+This will work for dists named like "Dist-Name-1.0".
+
+=head2 path_class_dir
+
+Returns the class name used for L<Path::Class::Dir> objects.
+
+=head2 path_class_file
+
+Returns the class name used for L<Path::Class::File> objects.
+
+=head2 path_classify_dir
+
+This is a shortcut for returning an object representing the provided
+dir utilizing L</path_class_dir> and L</file_spec>.
+
+=head2 path_classify_file
+
+This is a shortcut for returning an object representing the provided
+file utilizing L</path_class_file> and L</file_spec>.
+
+=head2 perl_files
+
+Returns the subset of L</list_files> that look like perl files.
+Currently returns anything matching C</\.pm$/>
+
+B<TODO>: This should probably be customizable.
+
+=head2 physical_directory
+
+ $dir = $dist->physical_directory();
+ ($dir, @dir_files) = $dist->physical_directory(@files);
+
+Returns the path to a physical directory on the disk
+where the specified files (if any) can be found.
+
+For in-memory formats this will make a temporary directory
+and write the specified files (or all files) into it.
+
+The return value is the same as L</extract_into>:
+In scalar context the path to the directory is returned.
+In list context the (possibly adjusted) paths to any specified files
+are appended to the return value.
+
+=head2 remove_root_dir
+
+ my ($dir, @rel) = $dm->remove_root_dir(@files);
+
+If all the C<@files> are beneath the same root directory
+(as is normally the case) this will strip the root directory off of each file
+and return a list of the root directory and the stripped files.
+
+If there is no root directory the first element of the list will be C<undef>.
+
+=head2 required_attribute
+
+A single attribute that is required by the class.
+Subclasses can define this to make L</new> C<croak> if it isn't present.
+
+=head2 root
+
+Returns the root directory of the dist (if there is one).
+
+=head2 set_name_and_version
+
+This is a convenience method for setting the name and version
+if they haven't already been set.
+This is often called by L</determine_name_and_version>.
+
+=head2 version
+
+Returns the version if it could be determined from the dist.
+
+=head1 SEE ALSO
+
+=over 4
+
+=item *
+
+L<Dist::Metadata::Tar> - for examining a tar file
+
+=item *
+
+L<Dist::Metadata::Dir> - for a directory already on the disk
+
+=item *
+
+L<Dist::Metadata::Struct> - for mocking up a dist with perl data structures
+
+=back
+
+=head1 AUTHOR
+
+Randy Stauner <rwstauner@cpan.org>
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2011 by Randy Stauner.
+
+This is free software; you can redistribute it and/or modify it under
+the same terms as the Perl 5 programming language system itself.
+
+=cut
diff --git a/lib/Dist/Metadata/Struct.pm b/lib/Dist/Metadata/Struct.pm
new file mode 100644
index 0000000..bca5578
--- /dev/null
+++ b/lib/Dist/Metadata/Struct.pm
@@ -0,0 +1,131 @@
+# vim: set ts=2 sts=2 sw=2 expandtab smarttab:
+#
+# This file is part of Dist-Metadata
+#
+# This software is copyright (c) 2011 by Randy Stauner.
+#
+# This is free software; you can redistribute it and/or modify it under
+# the same terms as the Perl 5 programming language system itself.
+#
+use strict;
+use warnings;
+
+package Dist::Metadata::Struct;
+our $AUTHORITY = 'cpan:RWSTAUNER';
+# ABSTRACT: Enable Dist::Metadata for a data structure
+$Dist::Metadata::Struct::VERSION = '0.926';
+use Carp qw(croak carp); # core
+use parent 'Dist::Metadata::Dist';
+
+push(@Dist::Metadata::CARP_NOT, __PACKAGE__);
+
+
+sub required_attribute { 'files' }
+
+
+sub default_file_spec { 'Unix' }
+
+
+sub file_content {
+ my ($self, $file) = @_;
+ # TODO: should we croak if not found? would be consistent with Dir
+ my $content = $self->{files}{ $self->full_path($file) };
+
+ # 5.10: given(ref($content))
+
+ if( my $ref = ref $content ){
+ local $/; # do this here because of perl bug prior to perl 5.15 (7c2d9d0)
+ return $ref eq 'SCALAR'
+ # allow a scalar ref
+ ? $$content
+ # or an IO-like object
+ : $content->getline;
+ }
+ # else a simple string
+ return $content;
+}
+
+
+sub find_files {
+ my ($self) = @_;
+
+ return keys %{ $self->{files} };
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=for :stopwords Randy Stauner ACKNOWLEDGEMENTS TODO dist dists dir unix checksum checksums
+David Jeffrey Ryan Sawyer Steinbrunner Thalhammer X
+
+=head1 NAME
+
+Dist::Metadata::Struct - Enable Dist::Metadata for a data structure
+
+=head1 VERSION
+
+version 0.926
+
+=head1 SYNOPSIS
+
+ my $dm = Dist::Metadata->new(struct => {
+ files => {
+ 'lib/Mod.pm' => 'package Mod; sub something { ... }',
+ 'README' => 'this is a fake dist, useful for testing',
+ }
+ });
+
+=head1 DESCRIPTION
+
+This is a subclass of L<Dist::Metadata::Dist>
+to enable mocking up a dist from perl data structures.
+
+This is mostly used for testing
+but might be useful if you already have an in-memory representation
+of a dist that you'd like to examine.
+
+It's probably not very useful on it's own though,
+and should be used from L<Dist::Metadata/new>.
+
+=head1 METHODS
+
+=head2 new
+
+ $dist = Dist::Metadata::Struct->new(files => {
+ 'lib/Mod.pm' => 'package Mod; sub something { ... }',
+ });
+
+Accepts a C<files> parameter that should be a hash of
+C<< { name => content, } >>.
+Content can be a string, a reference to a string, or an IO object.
+
+=head2 default_file_spec
+
+C<Unix> is the default for consistency/simplicity
+but C<file_spec> can be overridden in the constructor.
+
+=head2 file_content
+
+Returns the string content for the specified name.
+
+=head2 find_files
+
+Returns the keys of the C<files> hash.
+
+=head1 AUTHOR
+
+Randy Stauner <rwstauner@cpan.org>
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2011 by Randy Stauner.
+
+This is free software; you can redistribute it and/or modify it under
+the same terms as the Perl 5 programming language system itself.
+
+=cut
diff --git a/lib/Dist/Metadata/Tar.pm b/lib/Dist/Metadata/Tar.pm
new file mode 100644
index 0000000..bc89f49
--- /dev/null
+++ b/lib/Dist/Metadata/Tar.pm
@@ -0,0 +1,99 @@
+# vim: set ts=2 sts=2 sw=2 expandtab smarttab:
+#
+# This file is part of Dist-Metadata
+#
+# This software is copyright (c) 2011 by Randy Stauner.
+#
+# This is free software; you can redistribute it and/or modify it under
+# the same terms as the Perl 5 programming language system itself.
+#
+use strict;
+use warnings;
+
+package Dist::Metadata::Tar;
+our $AUTHORITY = 'cpan:RWSTAUNER';
+# ABSTRACT: Enable Dist::Metadata for tar files
+$Dist::Metadata::Tar::VERSION = '0.926';
+use Archive::Tar 1 (); # 0.07 isn't good enough
+use Carp (); # core
+use parent 'Dist::Metadata::Archive';
+
+push(@Dist::Metadata::CARP_NOT, __PACKAGE__);
+
+sub file_content {
+ my ( $self, $file ) = @_;
+ return $self->archive->get_content( $self->full_path($file) );
+}
+
+sub find_files {
+ my ($self) = @_;
+ return
+ map { $_->full_path }
+ grep { $_->is_file }
+ $self->archive->get_files;
+}
+
+sub read_archive {
+ my ($self, $file) = @_;
+
+ my $archive = Archive::Tar->new();
+ $archive->read($file);
+
+ return $archive;
+}
+
+sub tar {
+ warn __PACKAGE__ . '::tar() is deprecated. Use archive() instead.';
+ return $_[0]->archive;
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=for :stopwords Randy Stauner ACKNOWLEDGEMENTS TODO dist dists dir unix checksum checksums
+David Jeffrey Ryan Sawyer Steinbrunner Thalhammer X
+
+=head1 NAME
+
+Dist::Metadata::Tar - Enable Dist::Metadata for tar files
+
+=head1 VERSION
+
+version 0.926
+
+=head1 SYNOPSIS
+
+ my $dist = Dist::Metadata->new(file => $path_to_archive);
+
+=head1 DESCRIPTION
+
+This is a subclass of L<Dist::Metadata::Dist>
+(actually of L<Dist::Metadata::Archive>)
+to enable determining the metadata from a tar file.
+
+This is probably the most useful subclass.
+
+It's probably not very useful on it's own though,
+and should be used from L<Dist::Metadata/new>.
+
+=for Pod::Coverage tar
+
+=for test_synopsis my $path_to_archive;
+
+=head1 AUTHOR
+
+Randy Stauner <rwstauner@cpan.org>
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2011 by Randy Stauner.
+
+This is free software; you can redistribute it and/or modify it under
+the same terms as the Perl 5 programming language system itself.
+
+=cut
diff --git a/lib/Dist/Metadata/Zip.pm b/lib/Dist/Metadata/Zip.pm
new file mode 100644
index 0000000..ba136b2
--- /dev/null
+++ b/lib/Dist/Metadata/Zip.pm
@@ -0,0 +1,95 @@
+# vim: set ts=2 sts=2 sw=2 expandtab smarttab:
+#
+# This file is part of Dist-Metadata
+#
+# This software is copyright (c) 2011 by Randy Stauner.
+#
+# This is free software; you can redistribute it and/or modify it under
+# the same terms as the Perl 5 programming language system itself.
+#
+use strict;
+use warnings;
+
+package Dist::Metadata::Zip;
+our $AUTHORITY = 'cpan:RWSTAUNER';
+# ABSTRACT: Enable Dist::Metadata for zip files
+$Dist::Metadata::Zip::VERSION = '0.926';
+use Archive::Zip 1.30 ();
+use Carp (); # core
+
+use parent 'Dist::Metadata::Archive';
+
+push(@Dist::Metadata::CARP_NOT, __PACKAGE__);
+
+sub file_content {
+ my ($self, $file) = @_;
+ my ($content, $status) = $self->archive->contents( $self->full_path($file) );
+ Carp::croak "Failed to get content of '$file' from archive"
+ if $status != Archive::Zip::AZ_OK();
+ return $content;
+}
+
+sub find_files {
+ my ($self) = @_;
+ return
+ map { $_->fileName }
+ grep { !$_->isDirectory }
+ $self->archive->members;
+}
+
+sub read_archive {
+ my ($self, $file) = @_;
+
+ my $archive = Archive::Zip->new();
+ $archive->read($file) == Archive::Zip::AZ_OK()
+ or Carp::croak "Failed to read zip file!";
+
+ return $archive;
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=for :stopwords Randy Stauner ACKNOWLEDGEMENTS TODO dist dists dir unix checksum checksums
+David Jeffrey Ryan Sawyer Steinbrunner Thalhammer X
+
+=head1 NAME
+
+Dist::Metadata::Zip - Enable Dist::Metadata for zip files
+
+=head1 VERSION
+
+version 0.926
+
+=head1 SYNOPSIS
+
+ my $dist = Dist::Metadata->new(file => $path_to_archive);
+
+=head1 DESCRIPTION
+
+This is a subclass of L<Dist::Metadata::Dist>
+(actually of L<Dist::Metadata::Archive>)
+to enable determining the metadata from a zip file.
+
+It's probably not very useful on it's own
+and should be used from L<Dist::Metadata/new>.
+
+=for test_synopsis my $path_to_archive;
+
+=head1 AUTHOR
+
+Randy Stauner <rwstauner@cpan.org>
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2011 by Randy Stauner.
+
+This is free software; you can redistribute it and/or modify it under
+the same terms as the Perl 5 programming language system itself.
+
+=cut