| Unix-Conf documentation | Contained in the Unix-Conf distribution. |
Unix::Conf::ConfIO - This is an internal module for handling line at a time I/O for configuration files, with caching, locking and security support.
Open a configuration file and get a Unix::Conf::ConfIO object.
use Unix::Conf;
my $conf = Unix::Conf->_open_conf (
NAME => 'some_file',
SECURE_OPEN => 1,
LOCK_STYLE => 'dotlock',
);
$conf->die ('DEAD') unless ($conf);
# use the file in various ways
print while (<$conf>);
$conf->setline (10, "this is the 11th line\n");
$conf->close ();
ConfIO is designed to be a class for handling I/O for configuration files with support for locking and security. At the time of creation all the data in the file is read in and stored in an array, where each line is assumed to be one line of the file. It is the responsibility of the user to ensure that while appending or setting lines, the data ends in a newline. While this is not enforced it could cause the lineno counter to get confused.
Arguments
NAME => 'PATHNAME',
FH => filehandle # filehandle, reference to a filehandle
MODE => FILE_OPEN_MODE, # default is O_RDWR | O_CREAT
PERMS => FILE_CREATION_PERMS, # default is 0600
LOCK_STYLE => 'flock'/'dotlock', # default is 'flock'
SECURE_OPEN => 0/1, # default is 0 (disabled)
PERSIST => 0/1, # default is 0 (disabled)
Class constructor. Creates a ConfIO object which is associated with the file. Releasing the object automatically syncs with the disk version of the file. Passing an open filehandle with FH creates a Unix::Conf::ConfIO object representing the open file. Take care to open FH in both read & write mode, because Unix::Conf::ConfIO reads in the whole file into an in core array as of now. MODE and PERMS are the same as for sysopen. LOCK_STYLE is for choosing between different locking methods. 'dotlock' is used for locking '/etc/passwd', '/etc/shadow', '/etc/group', '/etc/gshadow'. 'flock' is the default locking style. If the value of SECURE_OPEN is 1, it enables a check to see if PATHNAME is secure. PERSIST is used to keep files open till release () or release_all () is called even though the object may go out of scope in the calling code. It reduces the overhead of managing ancillary files. Otherwise the file locks associated with the file would be released for these anciallary files. TODO: Need to implement ability to accept open filehandles, IO::Handle, IO::File objects too. NOTE: This method should not be used directly. Instead use Unix::Conf::_open_conf () which has the same syntax.
Example
use Unix::Conf;
my $conf;
$conf = Unix::Conf->_open_conf (
NAME => '/etc/passwd',
SECURE_OPEN => 1,
LOCK_STYLE => 'dotlock',
) or $conf->die ("Could not open '/etc/passwd'");
# or attach a filehandle to a Unix::Conf object.
$conf = Unix::Conf->_open_conf (
FH => FH, # or any object that represents an open filehandle
) or $conf->die ("Could not attach FH");
Object method. Syncs the cache to disk and releases the lock on the file unless the file was opened with PERSIST enabled. However the cache of data is maintained in the hope that it will still be useful and obviate the necessity for a read of all the data. Returns true or an Err object in case of error.
Object method. Closes the file and releases the lock if opened with PERSIST. Returns true or an Err object in case of error.
Class method. Closes all files opened with PERSIST by a specific class. This can be called from the destructor for that class, allowing hassle free operation for ancillary files. Returns true or an Err object in case of error.
Object method. Mark the file cache as dirty explicitly.
Object method. Returns the name of the associated file.
Object method. Rewind the file to the beginning of the data.
Object method. Returns max lineno + 1.
Arguments
SCALAR_REF,
Object method. Pass reference to a scalar. The file data will be set to the value of the scalar. Returns true.
Object method. Returns reference to the cache array. NOTE: If the caller then changes the array in anyway it is his/her responsibility to mark the cache as dirty.
Arguments
ARRAY_REF,
Object method. Store the array referenced by ARRAY_REF as the file cache. It is assumed that each element of the file will be a line of data with a trailing newline, though it is not a necessity. Returns true or an Err object in case of error.
Object method. Delete all lines in the file. Returns true or an Err object in case of error.
Object method. Get/set the current lineno of the ConfIO object.
Object method. Returns the next line.
Arguments
LINENO,
SCALAR,
Object method. Set LINENO to value of SCALAR. Returns true or an Err object in case of error.
Arguments
LIST,
Object method. Append LIST to the end of the file. Returns true or an Err object in case of error.
Arguments
START_LINENO,
END_LINENO,
Object method. Delete from START_LINENO to END_LINENO including. Returns true or an Err object in case of error.
Object method. Rewind the current lineno pointer. Returns true.
Beta
This module should accept a scalar representing a file too.
None that I know of.
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with the program; if not, write to the Free Software Foundation, Inc. :
59 Temple Place, Suite 330, Boston, MA 02111-1307
Copyright (c) 2002, Karthik Krishnamurthy <karthik.k@extremix.net>.
| Unix-Conf documentation | Contained in the Unix-Conf distribution. |
# Class to cache configuration file I/O # # Copyright Karthik Krishnamurthy <karthik.k@extremix.net>
package Unix::Conf::ConfIO; use 5.6.0; use strict; use warnings; use Fcntl qw (:DEFAULT :mode :flock); use Unix::Conf; use overload '<>' => 'getline', 'bool' => '__interpret_as_bool', '""' => '__interpret_as_string', 'eq' => '__interpret_as_string'; # Cache of file keyed on filenames. Value is a hash reference. # { # NAME, # FH, # MODE, # PERMS, # DATA, # TIMESTAMP, # LINENO, # LOCK_STYLE, # DIRTY, # PERSIST, # IN_USE, # } # FH is true if the file is currently in use (the filehandle is stored there). # TIMESTAMP is used to see if the file contents have been changed. # PERSIST is used for those files that need to be held and not # released even when their destructors have been called. This is needed for # cases where the file obj goes out of scope, but the file is not closed # here thus not releasing the lock on that file. This eases the task of # maintaining the filehandle by the user of this module. my %File_Cache; # This hash contains an entry for every module that calls us. If persistent # open is called, such files are stored as values for the key, which is the # calling module name. Thus, when release_all is called, we know exactly which # persistent files are to be released. my %Calling_Modules;
sub open { my $class = shift (); my $args = { LOCK_STYLE => 'flock', MODE => O_RDWR | O_CREAT, PERMS => 0600, SECURE_OPEN => 0, PERSIST => 0, @_, }; my ($fh, $name, $timestamp, $retval); # do sanity check on the passed argument return (Unix::Conf->_err ('open', "neither filename nor filehandle passed")) unless (defined ($args->{FH}) || defined ($args->{NAME})); if ($args->{FH}) { return (Unix::Conf->_err ("open", "`$args->{LOCK_STYLE}' illegal with FH")) if ($args->{LOCK_STYLE} ne 'flock'); return (Unix::Conf->_err ("open", "`SECURE_OPEN' illegal with FH")) if ($args->{SECURE_OPEN}); # store the filehandle name as the name of the file. # this is needed for persistent opens where the handle # is cached in $Calling_Modules. $args->{NAME} = "$args->{FH}"; $args->{FILEHANDLE_PASSED} = 1; $fh = $File_Cache{$args->{NAME}} = $args unless (($fh = $File_Cache{$args->{NAME}})); # fh now contains the old ConfIO object if one with the same # name exists # if file is locked in our cache return Err return (Unix::Conf->_err ('open', "`$fh->{NAME}' already in use, locked")) if ($fh->{IN_USE}); $retval = __lock ($fh) or return ($retval); } elsif ($args->{NAME}) { $fh = $File_Cache{$args->{NAME}} = $args unless (($fh = $File_Cache{$args->{NAME}})); # fh now contains the old ConfIO object if one with the same # name exists # if file is locked in our cache return Err return (Unix::Conf->_err ('open', "`$fh->{NAME}' already in use, locked")) if ($fh->{IN_USE}); # if FH exists, file must be a persistent one. we call __checkpath # if SECURE_OPEN was specified, and not before. However any change in # modes we barf if ($fh->{FH}) { my $ret; $ret = __checkpath ($fh->{NAME}) or return ($ret) if ($args->{SECURE_OPEN} > $fh->{SECURE_OPEN}); my ($oldmode, $newmode); $oldmode = $fh->{MODE} & (O_RDONLY | O_WRONLY | O_RDWR); $newmode = $args->{MODE} & (O_RDONLY | O_WRONLY | O_RDWR); return (Unix::Conf->_err ('open', "mode is not the same as in the original open")) if ($oldmode != $newmode); } else { # file is not in cache, or is but had been previously closed. # we need to open file even if our cache has good data, and lock the file $fh->{FH} = __open ( $fh->{NAME}, $fh->{MODE}, $fh->{PERMS}, $fh->{SECURE_OPEN} ) or return ($fh->{FH}); unless ($retval = __lock ($fh)) { close ($fh->{FH}); return ($retval); } } } # check timestamp even if file was held in persistent store and locked $timestamp = (stat ($fh->{FH}))[9]; # if we had previously cached the file and it has not changed since # bless and return. if (!defined ($fh->{TIMESTAMP}) || $fh->{TIMESTAMP} != $timestamp) { # if we reach here, either we don't have the file in our cache, # the cache is stale. return (Unix::Conf->_err ("open")) unless (seek ($fh->{FH}, 0, 0)); my @lines = readline ($fh->{FH}); @$fh{'DATA', 'TIMESTAMP'} = (\@lines, $timestamp); } @$fh{'LINENO', 'DIRTY'} = (-1, 0); $fh->{IN_USE} = 1; # store files that the calling module wants persisted. subsequently # when release_all is called by the same module, these will be actually # released (locks) $Calling_Modules{__caller ()}{$fh->{NAME}} = 1 if ($fh->{PERSIST}); my $instance = $fh->{NAME}; my $obj = bless (\$instance, $class); return ($obj); }
# method instance. can also be called as a subroutine with the filename as the # first arg. sub close { my $self = shift (); my $file = ref ($self) ? $File_Cache{$$self} : $File_Cache{$self}; return (Unix::Conf->_err ('close', "object already closed")) unless ($file->{FH}); # sync file if dirty if ($file->{DIRTY}) { truncate ($file->{FH}, 0) or return (Unix::Conf->_err ("truncate")); sysseek ($file->{FH}, 0, 0) or return (Unix::Conf->_err ("sysseek")); # suppress warnings so that we don't get a warning about empty # array elements that we delete'ed. no warnings; syswrite ($file->{FH}, $_ ) for (@{$$file{DATA}}); } $file->{TIMESTAMP} = (stat ($file->{FH}))[8]; # store new timestamp. $file->{IN_USE} = 0; # if persistent file, do not close return (1) if ($file->{PERSIST}); close ($file->{FH}) || return (Unix::Conf->_err ('close')); __unlock ($file); delete ($File_Cache{$file->{NAME}}) if ($file->{FILEHANDLE_PASSED}); undef ($file->{FH}); return (1); } sub DESTROY { my $self = shift (); my $obj = $File_Cache{$$self}; my $retval; # if FH is still set, call close if ($obj->{FH}) { $retval = $self->close () or $retval->die ('Unix::Conf::ConfIO DESTRUCTOR failed'); } } sub secure_open { my $self = shift (); my $obj = $File_Cache{$$self}; return ($obj->{SECURE_OPEN}); }
sub release { my $self = shift (); my $obj = $File_Cache{$$self}; # if FH is still set, call close if ($obj->{FH}) { # clear PERSIST so that close below can actually close $obj->{PERSIST} = 0; my $caller = __caller (); # # check to see the sanity check works properly. # return (Unix::Conf->_err ('release', "This file was not opened with PERSIST by $caller")) unless (exists ($Calling_Modules{$caller}{$$self})); delete ($Calling_Modules{$caller}{$$self}); return ($self->close ()); } return (1); }
sub release_all { my $caller = __caller (); my ($obj, $ret); return (Unix::Conf->_err ('release_all', "No files were opened with PERSIST by $caller")) unless (exists ($Calling_Modules{$caller})); for (keys(%{$Calling_Modules{$caller}})) { $obj = $File_Cache{$_}; $obj->{PERSIST} = 0; # call method close with the filename as arg $ret = &close ($_) or return ($ret); } delete ($Calling_Modules{$caller}); return (1); }
sub dirty { my $self = $File_Cache{${shift ()}}; $self->{DIRTY} = 1; return (1); }
sub name { my $self = $File_Cache{${shift ()}}; return ($self->{FILENAME}); }
sub rewind { my $self = $File_Cache{${shift ()}}; $self->{LINENO} = -1; return (1); }
sub next_lineno { my $self = $File_Cache{${shift ()}}; return (scalar (@{$self->{DATA}})); }
sub set_scalar { no warnings; my $self = $File_Cache{${shift ()}}; # release the old data undef ($self->{DATA}); $self->{DATA} = [ split (/^/, ${$_[0]}) ]; $self->{DIRTY} = 1; return (1); }
sub getlines { my $self = $File_Cache{${shift ()}}; return ($self->{DATA}); }
sub setlines { my $self = $File_Cache{${shift ()}}; my $openmode = $self->{MODE} & O_ACCMODE; return (Unix::Conf->_err ('setlines', "file $self->{FH} not opened for writing")) if ($openmode != O_WRONLY && $openmode != O_RDWR); $self->{DATA} = shift; $self->{DIRTY} = 1; return (1); }
sub delete { my $self = $File_Cache{${shift ()}}; my $openmode = $self->{MODE} & O_ACCMODE; return (Unix::Conf->_err ('delete', "file $self->{FH} not opened for writing")) if ($openmode != O_WRONLY && $openmode != O_RDWR); $self->{LINENO} = -1; undef (@{$self->{DATA}}); $self->{DIRTY} = 1; return (1); }
sub lineno { my $self = $File_Cache{${shift ()}}; my $lineno = shift (); if (defined ($lineno)) { return (Unix::Conf->_err ('lineno', "argument passed not numeric")) if ($lineno !~ /^-?\d+$/); return (Unix::Conf->_err ('lineno', "`$lineno' value illegal")) if ($lineno < -1); my $max = $#{$self->{DATA}}; return (Unix::Conf->_err ('lineno', "argument passed out of bounds, max possible `$max'")) if ($lineno > $max); $self->{LINENO} = $lineno; return (1); } return ($self->{LINENO}); }
sub getline { my $self = $File_Cache{${shift ()}}; return ( $self->{DATA}[++($self->{LINENO})] ); }
sub setline { my $self = $File_Cache{${shift ()}}; my $openmode = $self->{MODE} & O_ACCMODE; return (Unix::Conf->_err ('setline', "file $self->{FH} not opened for writing")) if ($openmode != O_WRONLY && $openmode != O_RDWR); $self->{DATA}[$_[0]] = $_[1]; $self->{DIRTY} = 1; return (1); }
sub append { my $self = $File_Cache{${shift ()}}; my $openmode = $self->{MODE} & O_ACCMODE; return (Unix::Conf->_err ('append', "file $self->{FH} not opened for writing")) if ($openmode != O_WRONLY && $openmode != O_RDWR); push (@{$self->{DATA}}, @_); $self->{DIRTY} = 1; return (1); }
sub delete_lines { my $self = $File_Cache{${shift ()}}; my $openmode = $self->{MODE} & O_ACCMODE; return (Unix::Conf->_err ('delete_lines', "file $self->{FH} not opened for writing")) if ($openmode != O_WRONLY && $openmode != O_RDWR); return (Unix::Conf->_err ('delete_lines', "offset not specified")) unless (defined ($_[0])); my $start = $_[0]; my $end = $_[1] ? $_[1] : $start; delete (@{$self->{DATA}}[$start..$end]); $self->{DIRTY} = 1; return (1); }
sub ungetline { my $self = $File_Cache{${shift ()}}; ($self->{LINENO})--; return (1); } sub __caller { return ((caller (1))[0]); } # If a ConfIO object has a defined filehandle it is true, else false sub __interpret_as_bool { my $self = $File_Cache{${shift ()}}; return ($self->{IN_USE}); } sub __interpret_as_string { my $self = shift; return "$$self"; } sub __lock ($) { my ($fh) = @_; ($fh->{LOCK_STYLE} eq 'flock') && do { flock ($fh->{FH}, LOCK_EX | LOCK_NB) || return (Unix::Conf->_err ('flock')); return (1); }; ($fh->{LOCK_STYLE} eq 'dotlock') && do { return (__dotlock ($fh->{NAME})); }; } sub __unlock ($) { my ($fh) = @_; ($fh->{LOCK_STYLE} eq 'flock') && do { # no unlocking necessary. when the appropriate fh is released or # close called on it the lock will be automatically released. return (1); }; ($fh->{LOCK_STYLE} eq 'dotlock') && do { unlink ("$fh->{NAME}.lock") or return (Unix::Conf->_err ('unlink')); return (1); }; } # ARGUMENTS: filename to be locked # RETURN: BOOL indicating failure or success. # Locks files. # Create a unique file from the filename (filename.pid). Write our PID into # pidfile. link to filename.lock. If filename.lock nlink is 2 then we have # succeded, unlink pidfile. If link fails then read the PID from the (already) # existing lockfile. Post 0 to that PID. If no such process exists, lock file # is stale and hence unlink it. loop again. All of this is because, opening # the actual lock file and writing out the PID into it is not atomic. So we # create an tmp unique file, write out our PID into it and then try linking it # which is atomic, since it translates into a single system call. sub __dotlock ($) { my $file = shift; my ($pidfile, $lockfile) = ("$file.$$", "$file.lock"); my $retval; sysopen (FH, $pidfile, O_WRONLY | O_CREAT || O_EXCL, 0600) or return (Unix::Conf->_err ('sysopen')); print FH "$$\x00" or return (Unix::Conf->_err ('new')); CORE::close (FH) or return (Unix::Conf->_err ('close')); # keep looping until we lock or return inside loop until (link ($pidfile, $lockfile)) { my $pid; unless (sysopen (FH, $lockfile, O_RDONLY)) { $retval = Unix::Conf->_err ('sysopen'); goto ERR_RET; } $pid = <FH> || goto ERR_RET; $pid = substr ($pid, 0, -1); CORE::close (FH) || goto ERR_RET; # if process is alive unlink opened files and return undef if (kill (0, $pid)) { $retval = Unix::Conf->_err ('kill'); goto ERR_RET; } unlink ($lockfile); } $retval = __check_link_count ($pidfile); unlink ($pidfile); return $retval; ERR_RET: unlink ($pidfile); return ($retval); } # check link count of argument and return true if link count == 2 sub __check_link_count ($) { my $file = shift; my $nlink; (undef, undef, undef, $nlink) = stat ($file) or return (Unix::Conf->_err ('stat')); return (1) if ($nlink == 2); # failure. set _err and return failure return (Unix::Conf->_err ('__check_link_count', 'link count of $file is $nlink')); } # ARGUMENTS: file_path, mode, perms, secure # if secure is true then security checks are done on the pathname to see # if any component is writeable by anyone other than root. if so return # error. sub __open ($$$$) { my ($file_path, $mode, $perms, $secure) = @_; my ($fh, $ret); sysopen ($fh, $file_path, $mode, $perms) || return (Unix::Conf->_err ("sysopen ($file_path)")); if ($secure) { $ret = __checkpath ($file_path) or return ($ret); } return ($fh); } sub __checkpath ($) { my $file_path = $_[0]; my @chopped = split (/\//, $file_path); # if $chopped[0] is "" then the path was absolute. if ($chopped[0]) { # is using `pwd` safe ? my $cwd = `pwd`; chomp ($cwd); unshift (@chopped, split (/\//, $cwd)); } my ($uid, $gid, $mode); my $path = ""; foreach (@chopped) { # on the second iteration $path will be just '/'. $path .= ($path =~ /^\/$/) ? "$_" : "/$_"; ($mode, $uid, $gid) = (stat ($path))[2,4,5]; # check ownership return (Unix::Conf->_err ('__open', "$file_path resides in an insecure path ($path)")) if ($uid != 0 || $gid != 0); # check to see if others have write perms return (Unix::Conf->_err ('__open', "$file_path resides in an insecure path ($path)")) if ($mode & S_IWOTH); } return (1); } 1; __END__