Sponsoring The Perl Toolchain Summit 2025: Help make this important event another success Learn more

##----------------------------------------------------------------------------
## Module Generic - ~/lib/Module/Generic/File/IO.pm
## Version v0.1.3
## Copyright(c) 2022 DEGUEST Pte. Ltd.
## Author: Jacques Deguest <jack@deguest.jp>
## Created 2022/04/26
## Modified 2022/11/12
## 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;
use Fcntl;
use IO::File ();
use parent qw( Module::Generic IO::File );
use vars qw( $VERSION @EXPORT $THAW_REOPENS_FILE );
# use Nice::Try;
use Scalar::Util ();
use Want;
our @EXPORT = grep( /^(?:O_|F_GETFL|F_SETFL)/, @Fcntl::EXPORT );
push( @EXPORT, @{$Fcntl::EXPORT_TAGS{flock}}, @{$Fcntl::EXPORT_TAGS{seek}} );
our @EXPORT_OK = qw( wraphandle );
our $THAW_REOPENS_FILE = 1;
our $VERSION = 'v0.1.3';
};
use strict;
sub new
{
my $this = shift( @_ );
my $class = ( ref( $this ) || $this );
my $opts = {};
$opts = pop( @_ ) if( ref( $_[-1] ) eq 'HASH' );
my $args = [@_];
my $self;
# try-catch
local $@;
eval
{
$self = $class->IO::File::new( @_ );
};
if( $@ )
{
return( $this->error( "Error trying to open file \"", $_[0], "\" with arguments: '", join( "', '", @_[1..$#_] ), "': $@" ) );
}
$self or return( $this->error( "Unable to open file \"", $_[0], "\" with arguments: '", join( "', '", @_[1..$#_] ), "': $!" ) );
if( exists( $opts->{fileno} ) &&
defined( $opts->{fileno} ) &&
length( $opts->{fileno} ) )
{
my $fileno = CORE::delete( $opts->{fileno} );
# > +<, etc and r, w, r+
my $mode = 'r';
$mode = CORE::delete( $opts->{mode} ) if( exists( $opts->{mode} ) && defined( $opts->{mode} ) && length( $opts->{mode} ) );
my $rv;
# try-catch
local $@;
eval
{
$rv = $self->fdopen( $fileno, $mode );
};
if( $@ )
{
return( $this->error( "Error trying to open file \"", $_[0], "\" with arguments: '", join( "', '", @_[1..$#_] ), "': $@" ) );
}
$rv or return( $this->error( "Unable to fdopen using file descriptor ${fileno} and mode ${mode}: $!" ) );
}
*$self = { args => $args };
if( Want::want( 'OBJECT' ) )
{
return( $self->init( $opts ) );
}
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 $self = shift( @_ );
my $opts = {};
$opts = pop( @_ ) if( ref( $_[-1] ) eq 'HASH' );
*$self->{_init_strict_use_sub} = 1;
$self->Module::Generic::init( $opts ) || return( $self->pass_error );
return( $self );
}
sub args
{
my $self = shift( @_ );
return( *$self->{args} );
}
# This class does not convert to an HASH
sub as_hash { return( $_[0] ); }
sub autoflush { return( shift->_filehandle_method( 'autoflush', @_ ) ); }
sub binmode { return( shift->_filehandle_method( 'binmode', @_ ) ); }
sub blocking { return( shift->_filehandle_method( 'blocking', @_ ) ); }
sub can_read
{
my $self = shift( @_ );
my $dummy = 0;
my $flags = $self->fcntl( F_GETFL, $dummy );
return( $self->error( $! ) ) if( !defined( $flags ) );
return(1) if( ( $flags & O_RDWR ) );
return(1) if( ( $flags & O_RDONLY ) == O_RDONLY );
# or, extracting the mode from the bits
# return(1) if( !( $flags & O_ACCMODE ) );
return(0);
}
sub can_write
{
my $self = shift( @_ );
my $dummy = 0;
my $flags = $self->fcntl( F_GETFL, $dummy );
return( $self->error( $! ) ) if( !defined( $flags ) );
return( $flags & ( O_APPEND | O_WRONLY | O_CREAT | O_RDWR ) );
}
sub close { return( shift->_filehandle_method( 'close', @_ ) ); }
# sub constant { return( shift->_filehandle_method( 'constant', @_ ) ); }
sub eof { return( shift->_filehandle_method( 'eof', @_ ) ); }
# sub fcntl { return( shift->_filehandle_method( 'fcntl', @_ ) ); }
sub fcntl
{
my $self = shift( @_ );
return( $self->error( 'usage: $io->fcntl( OP, VALUE );' ) ) if( scalar( @_ ) != 2 );
my( $op, $value ) = @_;
my $rv;
# try-catch
local $@;
eval
{
$rv = CORE::fcntl( *$self, $op, $value );
};
if( $@ )
{
return( $self->error( "An unexpected error occurred while trying to call fcntl with function '$op' and value '$value': $@" ) );
}
return( $rv );
}
sub fdopen { return( shift->_filehandle_method( 'fdopen', @_ ) ); }
sub fileno { return( shift->_filehandle_method( 'fileno', @_ ) ); }
sub flags
{
my $self = shift( @_ );
my $dummy;
# return( $self->fcntl( F_GETFL, $dummy ) );
return( CORE::fcntl( *$self, F_GETFL, $dummy ) );
}
sub flush { return( shift->_filehandle_method( 'flush', @_ ) ); }
sub format_formfeed { return( shift->_filehandle_method( 'format_formfeed', @_ ) ); }
sub format_line_break_characters { return( shift->_filehandle_method( 'format_line_break_characters', @_ ) ); }
sub format_lines_left { return( shift->_filehandle_method( 'format_lines_left', @_ ) ); }
sub format_lines_per_page { return( shift->_filehandle_method( 'format_lines_per_page', @_ ) ); }
sub format_name { return( shift->_filehandle_method( 'format_name', @_ ) ); }
sub format_page_number { return( shift->_filehandle_method( 'format_page_number', @_ ) ); }
sub format_top_name { return( shift->_filehandle_method( 'format_top_name', @_ ) ); }
sub format_write { return( shift->_filehandle_method( 'format_write', @_ ) ); }
sub formline { return( shift->_filehandle_method( 'formline', @_ ) ); }
sub getc { return( shift->_filehandle_method( 'getc', @_ ) ); }
sub getline { return( shift->_filehandle_method( 'getline', @_ ) ); }
sub getlines { return( shift->_filehandle_method( 'getlines', @_ ) ); }
sub getpos { return( shift->_filehandle_method( 'getpos', @_ ) ); }
sub input_line_number { return( shift->_filehandle_method( 'input_line_number', @_ ) ); }
sub input_record_separator { return( shift->_filehandle_method( 'input_record_separator', @_ ) ); }
sub ioctl { return( shift->_filehandle_method( 'ioctl', @_ ) ); }
sub new_from_fd { return( shift->_filehandle_method( 'new_from_fd', @_ ) ); }
sub new_tmpfile { return( shift->_filehandle_method( 'new_tmpfile', @_ ) ); }
sub opened { return( shift->_filehandle_method( 'opened', @_ ) ); }
sub output_field_separator { return( shift->_filehandle_method( 'output_field_separator', @_ ) ); }
sub output_record_separator { return( shift->_filehandle_method( 'output_record_separator', @_ ) ); }
sub print { return( shift->_filehandle_method( 'print', @_ ) ); }
sub printf { return( shift->_filehandle_method( 'printf', @_ ) ); }
sub printflush { return( shift->_filehandle_method( 'printflush', @_ ) ); }
sub read { return( shift->_filehandle_method( 'read', @_ ) ); }
sub say { return( shift->_filehandle_method( 'say', @_ ) ); }
sub seek { return( shift->_filehandle_method( 'seek', @_ ) ); }
sub setpos { return( shift->_filehandle_method( 'setpos', @_ ) ); }
sub stat { return( shift->_filehandle_method( 'stat', @_ ) ); }
sub sync { return( shift->_filehandle_method( 'sync', @_ ) ); }
sub sysread { return( shift->_filehandle_method( 'sysread', @_ ) ); }
sub sysseek { return( shift->_filehandle_method( 'sysseek', @_ ) ); }
sub syswrite { return( shift->_filehandle_method( 'syswrite', @_ ) ); }
sub tell { return( shift->_filehandle_method( 'tell', @_ ) ); }
sub truncate { return( shift->_filehandle_method( 'truncate', @_ ) ); }
sub ungetc { return( shift->_filehandle_method( 'ungetc', @_ ) ); }
sub untaint { return( shift->_filehandle_method( 'untaint', @_ ) ); }
sub wraphandle
{
my( $this, $mode ) = @_;
my $fileno;
if( Scalar::Util::blessed( $this ) &&
$this->can( 'fileno' ) )
{
$fileno = $this->fileno;
}
else
{
$fileno = CORE::fileno( $this );
}
if( !defined( $fileno ) )
{
warn( "Cannot get a file descriptor from the filehandle (${this}) provided.\n" );
return;
}
my $io = Module::Generic::File::IO->new( { 'fileno' => $fileno } ) || do
{
warn( Module::Generic::File::IO->error );
return;
};
return( $io );
}
sub write { return( shift->_filehandle_method( 'write', @_ ) ); }
sub _filehandle_method
{
my $self = shift( @_ );
# e.g. print, printf, seek, tell, rewinddir, close, etc
my $what = shift( @_ );
my @rv = ();
my $ref = IO::File->can( $what ) ||
return( $self->error( "Method '$what' is unsupported." ) );
# Check if it is opened.
# return( $self->error( "Calling ${what} on a closed filehandle." ) );
# return if( !defined( CORE::fileno( $self ) ) );
# if( !defined( CORE::fileno( $self ) ) )
# {
# warn( "Calling ${what} on a closed filehandle: ", $self->_get_stack_trace );
# return;
# }
no warnings 'uninitialized';
if( wantarray() )
{
local $@;
eval
{
@rv = $self->$ref( @_ );
};
if( $@ )
{
return( $self->error( "An unexpected error occurred while trying to call ${what} in list context: $@" ) );
}
}
else
{
local $@;
eval
{
$rv[0] = $self->$ref( @_ );
};
if( $@ )
{
return( $self->error( "An unexpected error occurred while trying to call ${what}: $@" ) );
}
}
return( $self->error({ skip_frames => 1, message => "Error with $what: $!" }) ) if( CORE::length( $! ) && ( !scalar( @rv ) || !CORE::defined( $rv[0] ) ) );
$self->clear_error;
return if( ( wantarray() && !scalar( @rv ) ) || ( !wantarray() && !defined( $rv[0] ) ) );
return( wantarray() ? @rv : $rv[0] );
}
sub DESTROY
{
# NOTE: Storable creates a dummy object as a SCALAR instead of GLOB, so we need to check.
shift->close if( ( Scalar::Util::reftype( $_[0] ) // '' ) eq 'GLOB' );
}
sub FREEZE
{
my $self = CORE::shift( @_ );
my $serialiser = CORE::shift( @_ ) // '';
my $class = CORE::ref( $self ) || $self;
my $args = $self->args;
# On or before Sereal version 4.023, Sereal did not support multiple values returned
CORE::return( [$class, \@$args] ) if( $serialiser eq 'Sereal' && Sereal::Encoder->VERSION <= version->parse( '4.023' ) );
CORE::return( $class, \@$args )
}
# NOTE: There cannot be a STORABLE_freeze subroutine, or else Storable would trigger an error "Unexpected object type (8) in store_hook()". So Storable must do it by itself, which means it will die or if $Storable::forgive_me is set to a true value, it will instead create a SCALAR instance of this class containing a string like "You lost GLOB(0x5616db45e4e8)"
# sub STORABLE_freeze { return( shift->FREEZE( @_ ) ); }
#
# sub STORABLE_thaw { return( shift->THAW( @_ ) ); }
# NOTE: STORABLE_freeze_pre_processing called by Storable::Improved
sub STORABLE_freeze_pre_processing
{
my $self = CORE::shift( @_ );
my $class = CORE::ref( $self ) || $self;
my $args = $self->args;
# We change the glob object into a regular hash-based one to be Storable-friendly
my $this = CORE::bless( { args => $args, class => $class } => $class );
CORE::return( $this );
}
sub STORABLE_thaw_post_processing
{
my $self = CORE::shift( @_ );
my $args = ( CORE::exists( $self->{args} ) && CORE::ref( $self->{args} ) eq 'ARRAY' )
? $self->{args}
: [];
my $class = ( CORE::exists( $self->{class} ) && CORE::defined( $self->{class} ) && CORE::length( $self->{class} ) )
? $self->{class}
: ( CORE::ref( $self ) || $self );
# We restore our glob object. Geez that was hard. Not.
my $obj = $THAW_REOPENS_FILE ? $class->new( @$args ) : $class->new;
return( $obj );
}
# NOTE: THAW is called by Sereal and CBOR
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 );
$ref = ( CORE::scalar( @$ref ) && CORE::ref( $ref->[0] ) eq 'ARRAY' ) ? $ref->[0] : [];
my $new;
if( $THAW_REOPENS_FILE && CORE::defined( $ref ) && CORE::ref( $ref ) eq 'ARRAY' )
{
$new = $class->new( @$ref );
}
else
{
$new = $class->new;
}
CORE::return( $new );
}
1;
# NOTE: POD
__END__
=encoding utf-8
=head1 NAME
Module::Generic::File::IO - File IO Object Wrapper
=head1 SYNOPSIS
use Module::Generic::File::IO;
my $io = Module::Generic::File::IO->new ||
die( Module::Generic::File::IO->error, "\n" );
my $io = Module::Generic::File::IO->new( fileno => $fileno ) ||
die( Module::Generic::File::IO->error, "\n" );
use Module::Generic::File::IO qw( wraphandle );
my $io = wraphandle( $fh );
my $io = wraphandle( $fh, '>' );
=head1 VERSION
v0.1.3
=head1 DESCRIPTION
This is a thin wrapper that inherits from L<IO::File> with the purpose of providing a uniform api in conformity with standard api call throughout the L<Module::Generic> modules family and to ensure call to any L<IO::File> will never die, but instead set an L<error|Module::Generic/error> and return C<undef>
Supported methods are rigorously the same as L<IO::File> and L<IO::Handle> on top of all the standard ones from L<Module::Generic>
The IO methods are listed below for convenience, but make sure to check the L<IO::File> documentation for more information.
=head1 CONSTRUCTOR
=head2 new
This instantiates a new L<Module::Generic::File::IO> object and returns it.
It optionally takes the following parameters:
=over 4
=item C<fileno>
A file descriptor. When this is provided, the newly created object will perform a L</fdopen> on the file descriptor provided.
=item C<mode>
A mode which will be used along with C<fileno> to fdopen the file descriptor. Possible values can be C<< < >>, C<< +< >>, C<< >+ >>, C<< +> >>, etc and C<r>, C<r+>, C<w>, C<w+>. C<a> and C<a+>
=back
=head1 FUNCTIONS
=head2 wraphandle
my $io = Module::Generic::File::IO::wraphandle( $fh, '>' );
# or
use Module::Generic::File::IO qw( wraphandle );
my $io = wraphandle( $fh, '>' );
Provided with a filehandle and an optional mode and this will return a newly created L<Module::Generic::File::IO>
By default, the mode will be '<'
=head1 METHODS
=head2 args
Returns an array reference containing the original arguments passed during object instantiation.
=head2 autoflush
See L<IO::Handle/autoflush> for details
=head2 binmode
See L<IO::File/binmode> for details
=head2 blocking
See L<IO::Handle/blocking> for details
=head2 can_read
Returns true if one can read from this filehandle, or false otherwise.
=head2 can_write
Returns true if one can write from this filehandle, or false otherwise.
=head2 close
See L<IO::Handle/close> for details
=head2 eof
See L<IO::Handle/eof> for details
=head2 fcntl
See L<IO::Handle/fcntl> for details
=head2 fdopen
See L<IO::Handle/fdopen> for details
=head2 fileno
See L<IO::Handle/fileno> for details
=head2 flags
Returns the filehandle flags value using L<perlfunc/fcntl>
=head2 flush
See L<IO::Handle/flush> for details
=head2 format_formfeed
See L<IO::Handle/format_formfeed> for details
=head2 format_line_break_characters
See L<IO::Handle/format_line_break_characters> for details
=head2 format_lines_left
See L<IO::Handle/format_lines_left> for details
=head2 format_lines_per_page
See L<IO::Handle/format_lines_per_page> for details
=head2 format_name
See L<IO::Handle/format_name> for details
=head2 format_page_number
See L<IO::Handle/format_page_number> for details
=head2 format_top_name
See L<IO::Handle/format_top_name> for details
=head2 format_write
See L<IO::Handle/format_write> for details
=head2 formline
See L<IO::Handle/formline> for details
=head2 getc
See L<IO::Handle/getc> for details
=head2 getline
See L<IO::Handle/getline> for details
=head2 getlines
See L<IO::Handle/getlines> for details
=head2 getpos
See L<IO::Seekable/getpos> for details
=head2 input_line_number
See L<IO::Handle/input_line_number> for details
=head2 input_record_separator
See L<IO::Handle/input_record_separator> for details
=head2 ioctl
See L<IO::Handle/ioctl> for details
=head2 new_from_fd
See L<IO::Handle/new_from_fd> for details
=head2 new_tmpfile
See L<IO::File/new_tmpfile> for details
=head2 opened
See L<IO::Handle/opened> for details
=head2 output_field_separator
See L<IO::Handle/output_field_separator> for details
=head2 output_record_separator
See L<IO::Handle/output_record_separator> for details
=head2 print
See L<IO::Handle/print> for details
=head2 printf
See L<IO::Handle/printf> for details
=head2 printflush
See L<IO::Handle/printflush> for details
=head2 read
See L<IO::Handle/read> for details
=head2 say
See L<IO::Handle/say> for details
=head2 seek
See L<IO::Seekable/seek> for details
=head2 setpos
See L<IO::Seekable/setpos> for details
=head2 stat
See L<IO::Handle/stat> for details
=head2 sync
See L<IO::Handle/sync> for details
=head2 sysread
See L<IO::Handle/sysread> for details
=head2 sysseek
See L<IO::Seekable/sysseek> for details
=head2 syswrite
See L<IO::Handle/syswrite> for details
=head2 tell
See L<IO::Seekable/tell> for details
=head2 truncate
See L<IO::Handle/truncate> for details
=head2 ungetc
See L<IO::Handle/ungetc> for details
=head2 untaint
See L<IO::Handle/untaint> for details
=head2 write
See L<IO::Handle/write> for details
=head1 CONSTANTS
L<Module::Generic::File::IO> automatically exports the following constants taken from L<Fcntl>:
=over 4
=item C<O_*>
=item C<F_GETFL>
=item C<F_SETFL>
=item C<LOCK_SH>
=item C<LOCK_EX>
=item C<LOCK_NB>
=item C<LOCK_UN>
=back
See also the manual page for C<fcntl> for more detail about those constants.
=head1 SERIALISATION
=for Pod::Coverage FREEZE
=for Pod::Coverage STORABLE_freeze
=for Pod::Coverage STORABLE_freeze_pre_processing
=for Pod::Coverage STORABLE_thaw_post_processing
=for Pod::Coverage STORABLE_thaw
=for Pod::Coverage THAW
=for Pod::Coverage TO_JSON
Serialisation by L<CBOR|CBOR::XS>, L<Sereal> and L<Storable::Improved> (or the legacy L<Storable>) is supported by this package. To that effect, the following subroutines are implemented: C<FREEZE>, C<THAW>
For C<STORABLE_freeze> and C<STORABLE_thaw>, they are not implemented, because as of version C<3.26> Storable raises an exception without giving any chance to the IO module to return an object representing the deserialised data. So, instead of using L<Storable>, use instead the drop-in replacement L<Storable::Improved>, which addresses and mitigate those issues.
If you use L<Storable::Improved>, then serialisation and deserialisation will work seamlessly.
Failure to do use L<Storable::Improved>, and L<Storable> would instead return the L<Module::Generic::File::IO> as a C<SCALAR> object rather than a glob.
Note that by default C<$THAW_REOPENS_FILE> is set to a true value, and this will have deserialisation recreate an object somewhat equivalent to the original one.
=head1 AUTHOR
Jacques Deguest E<lt>F<jack@deguest.jp>E<gt>
=head1 SEE ALSO
L<IO::Handle>, L<IO::File>, L<IO::Seekable>
=head1 COPYRIGHT & LICENSE
Copyright(c) 2022-2024 DEGUEST Pte. Ltd.
All rights reserved.
This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
=cut