The Perl and Raku Conference 2025: Greenville, South Carolina - June 27-29 Learn more

##----------------------------------------------------------------------------
## Module Generic - ~/lib/Module/Generic/Scalar/IO.pm
## Version v0.2.1
## Copyright(c) 2022 DEGUEST Pte. Ltd.
## Author: Jacques Deguest <jack@deguest.jp>
## Created 2022/04/24
## Modified 2022/08/05
## All rights reserved
##
## This program is free software; you can redistribute it and/or modify it
## under the same terms as Perl itself.
##----------------------------------------------------------------------------
BEGIN
{
use strict;
use warnings;
# For import of constants
use vars qw( $DEBUG $VERSION $ERROR @EXPORT );
no warnings 'once';
our @EXPORT = @Module::Generic::File::IO;
our $ERROR = '';
our $VERSION = 'v0.2.1';
};
use strict;
sub new
{
my $this = shift( @_ );
my $class = ( ref( $this ) || $this );
my $self;
# try-catch
local $@;
eval
{
$self = $class->IO::File::new;
};
if( $@ )
{
return( $self->error( "Error trying to get a file handle: $@" ) );
}
*$self = {};
if( Want::want( 'OBJECT' ) )
{
return( $self->init( @_ ) );
}
my $new = $self->init( @_ );
if( !defined( $new ) )
{
# If we are called on an object, we hand it the error so the caller can check it using the object:
# my $new = $old->new || die( $old->error );
if( $self->_is_object( $this ) && $this->can( 'pass_error' ) )
{
return( $this->pass_error( $self->error ) );
}
else
{
return( $self->pass_error );
}
};
return( $new );
}
sub init
{
my( $p, $f, $l ) = caller;
my $self = shift( @_ );
my $class = ( ref( $self ) || $self );
my( $ref, $mode );
if( @_ )
{
$ref = shift( @_ );
return( $self->error( "No scalar reference was provided." ) ) if( !defined( $ref ) );
return( $self->error( "I was expecting a scalar reference, but got a string of ", CORE::length( $ref ), " bytes instead." ) ) if( !ref( $ref ) );
return( $self->error( "I was expecting a scalar reference, but got instead '", overload::StrVal( $ref ), "'." ) ) if( !$self->_is_scalar( $ref ) );
$mode = ( scalar( @_ ) && ( ref( $_[0] ) ne 'HASH' || ( @_ > 2 && ( @_ % 2 ) ) ) ) ? shift( @_ ) : '+<';
$mode =~ s/^(.*?)\:$// if( substr( $mode, -1, 1 ) eq ':' );
}
else
{
my $str = '';
$ref = \$str;
$mode = '+<';
}
*$self->{sr} = $ref;
my $opts = $self->_get_args_as_hash( @_ );
my $core = {};
$core->{binmode} = CORE::delete( $opts->{binmode} ) if( exists( $opts->{binmode} ) );
$core->{autoflush} = CORE::delete( $opts->{autoflush} ) if( exists( $opts->{autoflush} ) );
$self->SUPER::init( @_ ) || return( $self->pass_error );
my $this = *$self;
# See PerlIO man page
if( defined( $core->{binmode} ) && length( $core->{binmode} ) )
{
# No need to specify the scalar layer, because we add it ourself
if( $core->{binmode} eq 'scalar' )
{
# no-op
}
elsif(
$core->{binmode} eq 'bytes' ||
$core->{binmode} eq 'crlf' ||
$core->{binmode} eq 'perlio' ||
$core->{binmode} eq 'raw' ||
$core->{binmode} eq 'stdio' ||
$core->{binmode} eq 'unix' ||
$core->{binmode} eq 'win32'
)
{
$mode .= ':' . $core->{binmode};
}
# others are encapsulated with :encoding() pragma, including utf8
else
{
$mode .= ':encoding(' . $core->{binmode} . ')';
}
}
$self->open( $ref => $mode ) || return( $self->pass_error );
$self->autoflush( $core->{autoflush} ) if( exists( $core->{autoflush} ) );
return( $self );
}
sub bit { return( *{shift( @_ )}->{bit} ); }
# Could also do: !( $_[0] & O_ACCMODE )
sub can_read { return( ( ( $_[0]->bit & O_RDONLY ) == O_RDONLY ) || ( $_[0]->bit & O_RDWR ) ); }
sub can_write { return( shift->bit & ( O_APPEND | O_WRONLY | O_CREAT | O_RDWR ) ); }
sub clearerr { return( shift->clear_error ); }
sub fcntl
{
my $self = shift( @_ );
my( $func, $bit ) = @_;
return( $self->error( "Function bit value is not an integer." ) ) if( !$self->_is_integer( $func ) );
if( $func & F_GETFL )
{
return( *$self->{bit} );
}
elsif( $func & F_SETFL )
{
return( $self->error( "Bitwise value provided '$bit' is not an integer." ) ) if( !$self->_is_integer( $bit ) );
*$self->{bit} = $bit;
}
else
{
return( $self->error( "Unknown fcntl function provided Please use either F_GETFL or F_SETFL" ) );
}
}
# Need to wrap the getline() method here, because it will not sto even when eof()
# has been reached and leading to the error: "Inappropriate ioctl for device"
# Thus here we wrap the getline() call and check for eof()
sub getline
{
my $self = shift( @_ );
return if( $self->eof );
return( $self->SUPER::getline() );
}
sub is_append { return( shift->bit & O_APPEND ); }
sub is_create { return( shift->bit & O_CREAT ); }
sub is_readonly { return( shift->bit == O_RDONLY ); }
sub is_readwrite { return( shift->bit & O_RDWR ); }
sub is_writeonly { return( shift->bit & O_WRONLY ); }
sub length
{
my $self = shift( @_ );
return( CORE::length( ${ *$self->{sr} } ) );
}
sub line
{
my $self = shift( @_ );
my $code = shift( @_ );
return( $self->error( "No callback code was provided for line()" ) ) if( !defined( $code ) || ref( $code ) ne 'CODE' );
my $opts = ref( $_[0] ) eq 'HASH' ? shift( @_ ) : { @_ };
return if( !$self->can_read );
$opts->{chomp} //= 0;
$opts->{auto_next} //= 0;
my $l;
while( defined( $l = $self->getline ) )
{
chomp( $l ) if( $opts->{chomp} );
local $_ = $l;
my $rv = $code->( $l );
if( !defined( $rv ) && !$opts->{auto_next} )
{
last;
}
}
return( $self );
}
sub object { return( *{ $_[0] }->{sr} ) }
sub open
{
my $self = shift( @_ );
my $class = ( ref( $self ) || $self );
return( $self->error( "open() is not a class function. You need to call it using a $class object." ) ) if( !ref( $self ) );
my $ref = shift( @_ );
return( $self->error( "No scalar reference was provided." ) ) if( !defined( $ref ) );
return( $self->error( "I was expecting a scalar reference, but got a string of ", CORE::length( $ref ), " bytes instead." ) ) if( !ref( $ref ) );
return( $self->error( "I was expecting a scalar reference, but got instead '", overload::StrVal( $ref ), "'." ) ) if( !$self->_is_scalar( $ref ) );
my $mode = shift( @_ ) ||
return( $self->error( "No mode was provided. Supported modes are: >, >>, +>, +>>, <, <+, r, r+, w, w+, a, a+" ) );
my $equi =
{
'r' => '<',
'r+' => '+<',
'w' => '>',
'w+' => '+>',
'a' => '>>',
'a+' => '+>>',
};
my $pl_mode = $mode;
if( index( $mode, ':' ) != -1 )
{
my @parts = split( /:/, $mode );
$mode = $parts[0];
$parts[0] = $equi->{ $parts[0] } if( CORE::exists( $equi->{ $parts[0] } ) );
# The order is important. :scalar needs to be the first IO layer
splice( @parts, 1, 0, 'scalar' ) if( !scalar( grep( $_ eq 'scalar', @parts ) ) );
# We only take the first part, i.e. the open mode and ignore the IO layer used for perl's open
$pl_mode = join( ':', @parts );
}
else
{
$pl_mode = $equi->{ $pl_mode } if( CORE::exists( $equi->{ $pl_mode } ) );
$pl_mode .= ':scalar';
}
no warnings 'uninitialized';
local $@;
my $rv = eval
{
open( $self, $pl_mode, $ref );
};
if( $@ )
{
return( $self->error( "Unable to open( $self, $pl_mode, ", overload::StrVal( $ref ), " ) scalar reference: $@" ) );
}
elsif( !$rv )
{
return( $self->error( "Unable to open( $self, $pl_mode, ", overload::StrVal( $ref ), " ) scalar reference: $!" ) );
}
my $bit;
my $bitmap =
{
'<' => O_RDONLY,
# Incorrect, but let's catch it anyway
'<+' => O_RDWR,
'+<' => O_RDWR,
'>' => ( O_CREAT | O_WRONLY ),
'+>' => ( O_CREAT | O_RDWR ),
'>>' => O_APPEND,
'+>>' => ( O_RDWR | O_APPEND ),
'r' => O_RDONLY,
'r+' => O_RDWR,
'w' => ( O_CREAT | O_WRONLY ),
'w+' => ( O_CREAT | O_RDWR ),
'a' => O_APPEND,
'a+' => ( O_RDWR | O_APPEND ),
};
# We set the bit for this glob, so fcntl works.
if( $mode =~ /^(<|<\+|\+<|>|\+>|>>|\+>>|r|r\+|w|w\+|a|a\+)$/ )
{
die( "Unable to find mode '$1' in our bitmap!\n" ) if( !CORE::exists( $bitmap->{ $1 } ) );
$bit = $bitmap->{ $1 };
if( $bit & O_CREAT )
{
$$ref = '' unless( !defined( $$ref ) );
}
}
else
{
return( $self->error( "Unsupported mode '$mode'" ) );
}
# If opened in read, even read/write mode, we position at the beginning of the string
*$self->{sr} = $ref;
# We use the bits to check what the methods are allowed to do
*$self->{bit} = $bit;
return( $self );
}
sub setpos { return( shift->seek( $_[0], 0 ) ); }
sub size { return( shift->length ); }
sub sref { return( shift->object ); }
# Missing method in IO::Scalar and not working under perl native open with IO::Handle
# It throws 'Bad file descriptor'
sub truncate
{
my $self = CORE::shift( @_ );
return if( !$self->can_write );
my $pos = $self->tell;
return( CORE::length( CORE::substr( ${*$self->{sr}}, $pos, CORE::length( ${*$self->{sr}} ) - $pos, '' ) ) );
}
sub sysread { return( shift->read( @_ ) ); }
sub syswrite { return( shift->write( @_ ) ); }
sub write
{
my $self = $_[0];
my $n = $_[2] // CORE::length( $_[1] );
my $off = $_[3] || 0;
return( $self->error( "Wrong number of parameters. Usage: \$io->write( \$buffer, \$length, \$offset ); \$offset is optional." ) ) if( @_ < 2 || @_ > 4 );
return if( !$self->can_write );
if( @_ == 4 )
{
$n = ( CORE::length( $_[1] ) - $off ) if( ( $off + $n ) > CORE::length( $_[1] ) );
}
else
{
$n = CORE::length( $_[1] ) if( $n > CORE::length( $_[1] ) );
}
$self->print( substr( $_[1], $off, $n ) ) || return( $self->pass_error );
return( $n );
}
sub DESTROY
{
shift->close;
}
sub FREEZE
{
my $self = CORE::shift( @_ );
my $serialiser = CORE::shift( @_ ) // '';
my $class = CORE::ref( $self ) || $self;
my %hash = %{*$self};
# Return an array reference rather than a list so this works with Sereal and CBOR
# On or before Sereal version 4.023, Sereal did not support multiple values returned
CORE::return( [$class, \%hash] ) if( $serialiser eq 'Sereal' && Sereal::Encoder->VERSION <= version->parse( '4.023' ) );
# But Storable want a list with the first element being the serialised element
CORE::return( $class, \%hash );
}
sub THAW
{
my( $self, undef, @args ) = @_;
my $ref = ( CORE::scalar( @args ) == 1 && CORE::ref( $args[0] ) eq 'ARRAY' ) ? CORE::shift( @args ) : \@args;
my $class = ( CORE::defined( $ref ) && CORE::ref( $ref ) eq 'ARRAY' && CORE::scalar( @$ref ) > 1 ) ? CORE::shift( @$ref ) : ( CORE::ref( $self ) || $self );
my $hash = CORE::ref( $ref ) eq 'ARRAY' ? CORE::shift( @$ref ) : {};
my $new = $class->new;
foreach( CORE::keys( %$hash ) )
{
*$new->{ $_ } = CORE::delete( $hash->{ $_ } );
}
CORE::return( $new );
}
1;
__END__