package RISCOS::ALF::Time;
use strict;
use vars qw ($VERSION);

$VERSION = 0.01;

sub new ($$) {
    my $proto = shift;
    my $class = ref($proto) || $proto;
    my $self  = {};
    my ($data) = @_;
    return undef unless defined ($data) && (length $data == 8);
 
    my ($t1, $micro, $t2) = unpack 'a4Sa2', $data;
    # erk. Little endian short

    $self->{__TIME} = $t2 . $t1;
    $self->{__MICRO} = $micro if $micro;	# Don't store it if it is zero
    bless ($self, $class);
}

sub Time {
    my $self = shift;
    $self->{__TIME};
}

sub Micro {
    my $self = shift;
    $self->{__MICRO} ||= 0;
}

sub TimeValid {
    my $self = shift;
    return 0 unless $self->{__TIME} =~ /[:-ÿ]\0$/s;	# 1980 to 2238
    defined $self->{__MICRO} ? 1 : 2;	# Most valid if microseconds == 0
}

package RISCOS::ALF::DirEntry;

use Carp;
use strict;
use vars qw (@ISA $VERSION);

$VERSION = 0.01;
@ISA = qw();

# Pass a ref to scalar data, and an offset, or real data
sub new ($$$;$$) {
    my $proto = shift;
    my $class = ref($proto) || $proto;
    my $self  = {};
    my ($chunkdata, $chunkfile, $length, $offset) = @_;

    return undef unless defined $chunkdata;
    my $what = 'chunk';
    if (ref $chunkdata eq 'SCALAR' or ref $chunkdata eq 'LVALUE') {
	# Hmm, passed a reference into something bigger
	if ($offset + 16 >= length $$chunkdata) {
	    return wantarray ? () : undef;	# OK, maybe not :-)
	}
	$what = 'reference';
	$length = unpack 'V', substr $$chunkdata, $offset + 4, 4;
	$chunkdata = substr $$chunkdata, $offset, $length;
    }

    if ($^W and defined ($length) and length $chunkdata != $length) {
	carp "${class}::new passed $what with length reported as " . $length
	  . ", actualy " . length $chunkdata;
    }
    $length = length $chunkdata;
    carp "$class length $length is not a multiple of 4" if $^W and $length & 3;
    if ($length < 16) {
	carp "$class length $length < 16" if $^W;
	return wantarray ? () : undef;
    }
    my ($chunkindex, $entrylen, $datalen, $data) = unpack 'V3a*', $chunkdata;

    if ($entrylen != $length) {
	carp "$class entry reports length as $entrylen, data supplied is "
	  . $length if $^W;
	return wantarray ? () : undef;
    }
    carp "$class data length of $datalen is too great for total length $length"
      if ($^W && $datalen > ($entrylen - 12));

    return undef unless defined (my ($name) = $data =~ /^([^\0]*)/s);
    $self->{__INDEX} = $chunkindex;
    $self->{__NAME} = $name;
    $self->{__CHUNK} = $chunkfile->By_Number ($chunkindex);

    my $time = new RISCOS::ALF::Time substr ($data, (length ($name) + 4) & ~3);
    $self->{__TIME} = $time if defined $time;
    
    return bless ($self, $class) unless wantarray;
    (bless ($self, $class), $length, $name)
}

sub Name {
    my $self = shift;
    $self->{'__NAME'};
}

sub Index {
    my $self = shift;
    $self->{'__INDEX'};
}

sub Time {
    my $self = shift;
    $self->{'__TIME'};
}

sub Chunk ($;$) {
    my $self = shift;
    my $chunk = $self->{'__CHUNK'};
    $self->{'__CHUNK'} = $_[0] if defined $_[0];
    $chunk;
}

package RISCOS::ALF;

require RISCOS::Chunkfile;
use Carp;
use strict;
use vars qw (@ISA $VERSION);

$VERSION = 0.02;
@ISA = 'RISCOS::Chunkfile';

sub new ($$) {
    my $proto = shift;
    my $class = ref($proto) || $proto;
    
    my $self = $class->SUPER::new ($_[0]);
    
    foreach my $what (qw(LIB_DIRY LIB_TIME LIB_VSRN OFL_SYMT OFL_TIME)) {
	if ($self->Multiple ($what)) {
	    carp "AOF file has '$what' chunks at positions "
		 . join (' ', @{$self->Lookup ($what)})
	      if $^W;
	    return undef;
	}
    }

    my $chunk = $self->Chunk ('LIB_VRSN');
    
    if (defined $chunk) {
	my $version  = unpack 'I', $chunk->Data();
	unless (4 == $chunk->Length) {
	    carp 'LIB_VRSN length ' . $chunk->Length() . ' != 4' if $^W;
	    return undef;
	}
	$self->{'__VERSION'} = $version;
    }
     
    unless (defined ($chunk = $self->Chunk ('LIB_DIRY'))) {
	carp "ALF file has no 'LIB_DIRY'" if $^W;
	return undef;
    }
    
    $chunk = \$chunk->Data;	# Ref to scalar data

    my $entries = {};
    my ($position, $entry, $length, $name) = (0);
    while ((($entry, $length, $name)
	      = new RISCOS::ALF::DirEntry $chunk, $self, undef, $position),
	    defined $entry) {
	$entries->{$name} = $entry;
	$position += $length
    }
    $self->{'__DIR'} = $entries;

    if (defined ($chunk = $self->Chunk ('LIB_TIME'))) {
	my $time = new RISCOS::ALF::Time $chunk->Data;
	$self->{'__TIME'} = $time if defined $time;
    }
    
    if (defined ($chunk = $self->Chunk ('OFL_TIME'))) {
	my $time = new RISCOS::ALF::Time $chunk->Data;
	$self->{'__SYM_TIME'} = $time if defined $time;
    }
    $self;
}

sub Dir ($) {
    my $self = shift;
    $self->{'__DIR'};
}

sub Dir_Lookup ($$) {
    my $self = shift;
    $self->{'__DIR'}->{$_[0]};
}

sub Time ($) {
    my $self = shift;
    $self->{'__TIME'};
}

sub Sym_Time ($) {
    my $self = shift;
    $self->{'__TIME'};
}

sub Version ($) {
    my $self = shift;
    $self->{'__VERSION'};
}

1;
__END__

=head1 NAME

RISCOS::ALF -- manipulate Acorn Library Format files

=head1 SYNOPSIS

    use RISCOS::ALF;
    
    $library = new RISCOS::ALF $file;
    print "Library file '$file':\n";
    print '  ALF version ', $library->Version, "\n";

=head1 DESCRIPTION

C<RISCOS::ALF> provides a class derived from C<RISCOS::Chunkfile> to manipulate
the contents of B<A>corn B<L>ibrary B<F>ormat files. It provides a classes
C<RISCOS::ALF::Time> to manipulate 8 byte C<ALF> microsecond timestamps and
C<RISCOS::ALF::DirEntry> to store details of each directory entry in an C<ALF>
file.

Currently the implementation does not decode the library's symbol table.

=head2 Methods

=over 4

=item new <array_ref>

=item new <file>

If passed a reference to an array it is assumed to be an array of
C<RISCOS::Chunk> objects to use as the file contents. Otherwise calls
C<RISCOS::File::load> to loads the file specified using and checks that it is
C<ALF>. Hence I<file> can be a filename, a reference to a filehandle, or a
reference to a scalar which is used as the file's contents.

If passed an array reference then this is used internally in the object, so
should be created with the anonymous array constructor C<[]> rather than a
reference to a named array variable (see  L<perldsc/Common Mistakes>).

Returns undefined if there was an error, or the file contents are corrupt.

=item Dir

Returns a reference to the hash of C<RISCOS::ALF::DirEntry> objects describing
the C<ALF> file's directory. The keys are member names, the values the objects.

=item Dir_Lookup <name>

Returns the C<RISCOS::ALF::DirEntry> object for I<name> in the C<ALF> file's
directory.

=item Time

Returns the C<RISCOS::ALF::Time> object that gives the library timestamp - the
time when the library was last modified.

=item Sym_Time

Returns the C<RISCOS::ALF::Time> object that gives the library symbol table
timestamp (or undefined if there is no symbol table) - the time when the symbol
table was last modified.

=item Version

Returns the version number of the C<ALF> library. The current version is 1.

=back

=head1 RISCOS::ALF::Time

The C<RISCOS::ALF::Time> class is used to hold 8 byte microsecond timestamps as
used in C<ALF> libraries. It provides the following methods:

=over 4

=item new <packed_timestamp>

Creates a new C<RISCOS::ALF::Time> object from the 8 byte packed timestamp in an
C<ALF> library.

=item Time

Returns the B<6> byte timestamp as centiseconds from the start of 1900. Assuming
little endian byte order the last character should be C<"\0"> - chop this to get
a standard B<5> byte RISCOS timestamp. Note that C<RISCOS::Time> functions will
perform this conversion automatically.

=item Micro

Returns the microseconds field of the timestamp. This is usually 0.

=item TimeValid

Performs a sanity check on the time stored in the object. Returns:

=over 4

=item 0 if the date is outside the range (roughly) 1980 - 2238

=item 1 if the date is in this range but the microseconds field is non-zero

=item 2 if the date is in this range and the microseconds field is zero

=back

=back

=head1 RISCOS::ALF::DirEntry

The C<RISCOS::ALF::DirEntry> class is used to hold details of directory entries
in C<ALF> libraries. It provides the following methods:

=over 4

=item new <scalar_reference>, <chunkfile>, undef, <offset>

=item new <file_data>, <chunkfile> ,<length>]

Creates a new C<RISCOS::ALF::DirEntry> from the supplied data. If passed a
scalar this will be treated as being a single 'C<DATA>' item from a
'C<LIB_DIRY>' chunk in an C<ALF> file. I<length>, if defined, is used to verify
the length recorded in the 'C<DATA>' item.

If passed a reference to a scalar, then this is dereferenced and the I<offset>
parameter used to locate the 'C<DATA>' within this scalar. This allows an
entire 'C<LIB_DIRY>' chunk to be converted with minimal string copying.
(Remember that the length of a 'C<DATA>' item is unknown until decoding
commences, so it is not possible to pass a substring to C<new> without I<a
priori> knowledge of the length.)

I<chunkfile> is the C<RISCOS::Chunkfile> object that contains the chunk that
this directory entry referes to.

In scalar context returns the object. In B<array context> returns
C<(object, length, name)>. I<length> is added to the current I<offset> to
calculate the offset of the next 'C<DATA>' item.

=item Name

Returns the name of this object, usually the file name whose contents form the
data for this library member.

=item Index

Returns the index of the chunk which holds the data for this member.

=item Time

Returns the C<RISCOS::ALF::Time> object that gives the timestamp for this
member. This is the last-modified time of the file whose contents form the data
for this library member.

=item Chunk

Returns the C<RISCOS::Chunk> object for this entry.

=back

=head1 BUGS

As noted, methods to manipulate the library symbol table are currently
unimplemented. Wildcard matching on library members is not implemented.

=head1 AUTHOR

Nicholas Clark <F<nick@unfortu.net>>
