=head1 NAME

Linux::Ext2::Attributes - Linux ext2/3/4 filesystem attributes.

=head1 SYNOPSIS

  use Linux::Ext2::Attributes qw(set_attrs :flags);
  
  set_attrs("somefile", "i");
  
  my $attribs = Linux::Ext2::Attributes->load("somefile");
  
  my $attribs_i = $attribs->flags;   # 16
  my $attribs_s = $attribs->string;  # "i"
  
  $attribs->set("-i");
  
  $attribs->save("somefile");

=head1 DESCRIPTION

Linux::Ext2::Attributes provides an object-oriented interface for manipulating
Linux extended filesystem attributes and a chattr-like function.

Only regular files/directories can have attributes.

=cut

package Linux::Ext2::Attributes;

use strict;
use warnings;

our $VERSION = "0.20";

use Scalar::Util qw(looks_like_number);
use Carp;

use Exporter "import";

our @EXPORT_OK = qw(
	set_attrs
	
	EXT2_NOATIME_FL
	EXT2_APPEND_FL
	EXT2_COMPRBLK_FL
	EXT2_COMPR_FL
	EXT2_DIRSYNC_FL
	EXT2_NODUMP_FL
	EXT2_ECOMPR_FL
	EXT4_EXTENTS_FL
	EXT4_HUGE_FILE_FL
	EXT2_INDEX_FL
	EXT2_IMMUTABLE_FL
	EXT3_JOURNAL_DATA_FL
	EXT2_SECRM_FL
	EXT2_SYNC_FL
	EXT2_TOPDIR_FL
	EXT2_NOTAIL_FL
	EXT2_UNRM_FL
	EXT2_NOCOMP_FL
	EXT2_DIRTY_FL
);

our %EXPORT_TAGS = (
	"flags" => [ qw(
		EXT2_NOATIME_FL
		EXT2_APPEND_FL
		EXT2_COMPRBLK_FL
		EXT2_COMPR_FL
		EXT2_DIRSYNC_FL
		EXT2_NODUMP_FL
		EXT2_ECOMPR_FL
		EXT4_EXTENTS_FL
		EXT4_HUGE_FILE_FL
		EXT2_INDEX_FL
		EXT2_IMMUTABLE_FL
		EXT3_JOURNAL_DATA_FL
		EXT2_SECRM_FL
		EXT2_SYNC_FL
		EXT2_TOPDIR_FL
		EXT2_NOTAIL_FL
		EXT2_UNRM_FL
		EXT2_NOCOMP_FL
		EXT2_DIRTY_FL
	) ]
);

# Load all the constants generated by H2PM into our own namespace.

use Linux::Ext2::Attributes::Constants;

use constant EXT2_IOC_GETFLAGS => Linux::Ext2::Attributes::Constants::EXT2_IOC_GETFLAGS;
use constant EXT2_IOC_SETFLAGS => Linux::Ext2::Attributes::Constants::EXT2_IOC_SETFLAGS;

use constant EXT2_NOATIME_FL      => Linux::Ext2::Attributes::Constants::EXT2_NOATIME_FL;
use constant EXT2_APPEND_FL       => Linux::Ext2::Attributes::Constants::EXT2_APPEND_FL;
use constant EXT2_COMPRBLK_FL     => Linux::Ext2::Attributes::Constants::EXT2_COMPRBLK_FL;
use constant EXT2_COMPR_FL        => Linux::Ext2::Attributes::Constants::EXT2_COMPR_FL;
use constant EXT2_DIRSYNC_FL      => Linux::Ext2::Attributes::Constants::EXT2_DIRSYNC_FL;
use constant EXT2_NODUMP_FL       => Linux::Ext2::Attributes::Constants::EXT2_NODUMP_FL;
use constant EXT2_ECOMPR_FL       => Linux::Ext2::Attributes::Constants::EXT2_ECOMPR_FL;
use constant EXT2_INDEX_FL        => Linux::Ext2::Attributes::Constants::EXT2_INDEX_FL;
use constant EXT2_IMMUTABLE_FL    => Linux::Ext2::Attributes::Constants::EXT2_IMMUTABLE_FL;
use constant EXT2_SECRM_FL        => Linux::Ext2::Attributes::Constants::EXT2_SECRM_FL;
use constant EXT2_SYNC_FL         => Linux::Ext2::Attributes::Constants::EXT2_SYNC_FL;
use constant EXT2_TOPDIR_FL       => Linux::Ext2::Attributes::Constants::EXT2_TOPDIR_FL;
use constant EXT2_NOTAIL_FL       => Linux::Ext2::Attributes::Constants::EXT2_NOTAIL_FL;
use constant EXT2_UNRM_FL         => Linux::Ext2::Attributes::Constants::EXT2_UNRM_FL;
use constant EXT2_NOCOMP_FL       => Linux::Ext2::Attributes::Constants::EXT2_NOCOMP_FL;
use constant EXT2_DIRTY_FL        => Linux::Ext2::Attributes::Constants::EXT2_DIRTY_FL;

# Constants pilfered out of the kernel headers as they don't seem to be commonly
# available in the userspace ones.

use constant EXT3_JOURNAL_DATA_FL => 0x00004000;
use constant EXT4_EXTENTS_FL      => 0x00080000;
use constant EXT4_HUGE_FILE_FL    => 0x00040000;

# These flags may not be set or cleared by the user.

use constant EXT2_READONLY_FLAGS  => (EXT2_ECOMPR_FL | EXT4_EXTENTS_FL | EXT4_HUGE_FILE_FL | EXT2_INDEX_FL | EXT2_NOCOMP_FL | EXT2_DIRTY_FL);

my %flag_chars = (
	'A' => EXT2_NOATIME_FL,
	'a' => EXT2_APPEND_FL,
	'B' => EXT2_COMPRBLK_FL,
	'c' => EXT2_COMPR_FL,
	'D' => EXT2_DIRSYNC_FL,
	'd' => EXT2_NODUMP_FL,
	'E' => EXT2_ECOMPR_FL,
	'e' => EXT4_EXTENTS_FL,
	'h' => EXT4_HUGE_FILE_FL,
	'I' => EXT2_INDEX_FL,
	'i' => EXT2_IMMUTABLE_FL,
	'j' => EXT3_JOURNAL_DATA_FL,
	's' => EXT2_SECRM_FL,
	'S' => EXT2_SYNC_FL,
	'T' => EXT2_TOPDIR_FL,
	't' => EXT2_NOTAIL_FL,
	'u' => EXT2_UNRM_FL,
	'X' => EXT2_NOCOMP_FL,
	'Z' => EXT2_DIRTY_FL,
);

sub _get_fh {
	my ($file) = @_;
	
	if(ref($file))
	{
		return $file;
	}
	
	open(my $fh, "<", $file) or return undef;
	
	return $fh;
}

sub _get_flags {
	my ($file) = @_;
	
	my $fh = _get_fh($file) or return undef;
	
	my $flags = pack("i", 0);
	
	if(!ioctl($fh, EXT2_IOC_GETFLAGS, $flags))
	{
		return undef;
	}
	
	return scalar unpack("i", $flags);
}

sub _set_flags {
	my ($file, $flags) = @_;
	
	$file = _get_fh($file) or return undef;
	
	$flags = pack("i", $flags);
	
	return ioctl($file, EXT2_IOC_SETFLAGS, $flags);
}

=head1 SUBROUTINES

=head2 set_attrs

  set_attrs($file, $attribs)
  set_attrs($file, EXT2_IMMUTABLE_FL | EXT2_NOATIME_FL)
  set_attrs($file, "iA")
  set_attrs($file, "-a+iA")

Set the attributes on a filename or open file. The attributes may be an instance
of Linux::Ext2::Attributes, an integer of bitwise OR'd flags or a string based
on the format used by the chattr program.

Not all attributes of a file may be changed. Any read-only attributes on a file
will remain unchanged.

Returns true on success, false on error.

=cut

sub set_attrs {
	my ($file, $attribs) = @_;
	
	my $new_flags = Linux::Ext2::Attributes->new($attribs);
	
	$file = _get_fh($file) or return undef;
	
	my $old_flags = _get_flags($file);
	
	if(!defined($old_flags))
	{
		return undef;
	}
	
	$new_flags->set(($old_flags & EXT2_READONLY_FLAGS) | $new_flags->strip->flags);
	
	return _set_flags($file, $new_flags->flags);
}

=head1 METHODS

=head2 new

  my $attribs = Linux::Ext2::Attributes->new()
  my $attribs = Linux::Ext2::Attributes->new($value)

Return a new instance of Linux::Ext2::Attributes containing no flags or an
arbitrary set.

=cut

sub new {
	my ($class, $value) = @_;
	
	my $self = bless(\do { my $self = 0; }, $class);
	
	if(defined($value))
	{
		$self->set($value);
	}
	
	return $self;
}

=head2 load

  my $attribs = Linux::Ext2::Attributes->load("filename")
  my $attribs = Linux::Ext2::Attributes->load($filehandle)
  my $attribs = Linux::Ext2::Attributes->load(\*FILE)

Get the attributes of a filename or open file. Returns an instance of
Linux::Ext2::Attributes on success, undef on error.

=cut

sub load {
	my ($class, $file) = @_;
	
	my $flags = _get_flags($file);
	
	return defined($flags) ? bless(\$flags, $class) : undef;
}

=head2 save

  $attribs->save("filename")
  $attribs->save($filehandle)
  $attribs->save(\*FILE)

Set the attributes of a filename or open file. Returns true on success, false
on failure.

=cut

sub save {
	my ($self, $file) = @_;
	
	return set_attrs($file, $self);
}

=head2 set

  $attribs->set($attribs)
  $attribs->set(EXT2_IMMUTABLE_FL | EXT2_NOATIME_FL)
  $attribs->set("iA")
  $attribs->set("-a+iA")

Replace or modify the stored flags value. Takes the same attributes as set_attrs.

=cut

sub set {
	my ($self, $attribs) = @_;
	
	if(ref($attribs))
	{
		$$self = $$attribs;
	}
	elsif(looks_like_number($attribs))
	{
		$$self = $attribs;
	}
	elsif($attribs =~ m/\A=?([AaBcDdEehIijsSTtuXZ]*)\z/)
	{
		$$self = 0;
		
		$attribs =~ s/=//;
		
		foreach my $flag(split(//, $attribs))
		{
			$$self |= $flag_chars{$flag};
		}
	}
	elsif($attribs =~ m/\A([-+][AaBcDdEehIijsSTtuXZ]*)*\z/)
	{
		my $add = 0;
		
		foreach my $flag(split(//, $attribs))
		{
			if($flag eq "-")
			{
				$add = 0;
			}
			elsif($flag eq "+")
			{
				$add = 1;
			}
			else{
				if($add)
				{
					$$self |= $flag_chars{$flag};
				}
				else{
					$$self &= ~$flag_chars{$flag};
				}
			}
		}
	}
	else{
		carp("Unknown flags passed to set: '$attribs'");
	}
}

=head2 flags

  my $attribs_i = $attribs->flags()

Return the attributes as a bitwise OR'd integer (e.g. 148).

=cut

sub flags {
	my ($self) = @_;
	
	return $$self;
}

=head2 string

  my $attribs_s = $attribs->string()

Return the attributes as a string of characters (e.g. "icA").

=cut

sub string {
	my ($self) = @_;
	
	my $string = "";
	
	foreach my $flag(keys(%flag_chars))
	{
		if($$self & $flag_chars{$flag})
		{
			$string .= $flag;
		}
	}
	
	return $string;
}

=head2 strip

  $attribs->strip()

Unset any read-only/system flags and return self.

=cut

sub strip {
	my ($self) = @_;
	
	$$self &= ~EXT2_READONLY_FLAGS;
	
	return $self;
}

sub _do_flag {
	my ($self, $flag, $value) = @_;
	
	if(defined($value))
	{
		if($value)
		{
			$$self |= $flag;
		}
		else{
			$$self &= ~$flag;
		}
	}
	
	return ($$self & $flag) ? 1 : 0;
}

=head2 immutable

  $attribs->immutable()
  $attribs->immutable(true/false value)

Get and/or set the state of the immutable flag. Returns the current/new value.

=cut

sub immutable {
	my ($self, $value) = @_;
	
	return $self->_do_flag(EXT2_IMMUTABLE_FL, $value);
}

=head2 append_only

  $attribs->append_only()
  $attribs->append_only(true/false value)

Get and/or set the state of the append only flag. Returns the current/new value.

=cut

sub append_only {
	my ($self, $value) = @_;
	
	return $self->_do_flag(EXT2_APPEND_FL, $value);
}

=head2 flag

  $attribs->flag(EXT3_JOURNAL_DATA_FL)
  $attribs->flag("j", true/false value)

Get and/or set the state of an arbitrary flag. Returns the current/new value.

=cut

sub flag {
	my ($self, $flag, $value) = @_;
	
	if(!looks_like_number($flag))
	{
		if(!defined($flag_chars{$flag}))
		{
			carp("Unknown flag passed to flag: '$flag'");
			return undef;
		}
		
		$flag = $flag_chars{$flag};
	}
	
	return $self->_do_flag($flag, $value);
}

=head1 FLAGS

The following flag constants are defined and may be imported using the :flags
tag. Not all of them may be modified by the user or are currently implemented
in the Linux kernel. For more information see the chattr man page.

  EXT2_NOATIME_FL      (A) - Do not update atime on access.
  EXT2_APPEND_FL       (a) - File may only be appended to.
  EXT2_COMPRBLK_FL     (B) - One or more compressed clusters.
  EXT2_COMPR_FL        (c) - Compress file on disk.
  EXT2_DIRSYNC_FL      (D) - Directory changes are synchronous.
  EXT2_NODUMP_FL       (d) - Not backed up by dump.
  EXT2_ECOMPR_FL       (E) - Compression error.
  EXT4_EXTENTS_FL      (e) - File is using extents for block mapping.
  EXT4_HUGE_FILE_FL    (h) - File is (or was) larger than 2TB.
  EXT2_INDEX_FL        (I) - Directory is indexed using hashed trees.
  EXT2_IMMUTABLE_FL    (i) - File may not be modified.
  EXT3_JOURNAL_DATA_FL (j) - Journal file data as well as metadata.
  EXT2_SECRM_FL        (s) - File will be securely deleted when unlinked.
  EXT2_SYNC_FL         (S) - Changes to this file are written synchronously.
  EXT2_TOPDIR_FL       (T) - Directory is at the top of a hierarchy.
  EXT2_NOTAIL_FL       (t) - Disable tail merging.
  EXT2_UNRM_FL         (u) - Keep file for undeletion.
  EXT2_NOCOMP_FL       (X) - Don't compress file.
  EXT2_DIRTY_FL        (Z) - Compressed file is dirty.

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2012,2013 Daniel Collins E<lt>solemnwarning@solemnwarning.netE<gt>

This library is free software; you may redistribute it and/or modify it under
the same terms as Perl itself.

=cut

1;
