#!/usr/bin/perl -w
use strict;

use Carp ();
local $SIG{'__WARN__'} = \&Carp::cluck;

my $has_threads = 0;
eval {
    require threads;
    require threads::shared;
    1;
} and do {
    $has_threads = 1;
    threads->import();
    threads::shared->import();
};

my $has_Filesys__Statvfs = 0;
eval {
    require Filesys::Statvfs;
    1;
} and do {
    $has_Filesys__Statvfs = 1;
    Filesys::Statvfs->import();
};

my $use_lchown = 0;
eval {
    require Lchown;
	1;
} and do {
	$use_lchown = 1;
	Lchown->import();
};

my $has_mknod = 0;
eval {
        require Unix::Mknod;
        1;
} and do {
        $has_mknod = 1;
	Unix::Mknod->import();
};

use blib;
use Filesys::Fuse3;
use IO::File;
use POSIX qw(ENOTDIR ENOENT ENOSYS EEXIST EPERM O_RDONLY O_RDWR O_APPEND O_CREAT setsid);
use Fcntl qw(S_ISBLK S_ISCHR S_ISFIFO SEEK_SET S_ISREG S_ISFIFO S_IMODE S_ISCHR S_ISBLK S_ISSOCK);
use Getopt::Long;

my %extraopts = ( 'threaded' => 0, 'debug' => 0 );
my($use_real_statfs, $pidfile, $logfile);
GetOptions(
    'use-threads'       => sub {
        if ($has_threads) {
            $extraopts{'threaded'} = 1;
        }
    },
    'debug'             => sub {
        $extraopts{'debug'} = 1;
    },
    'use-real-statfs'   => \$use_real_statfs,
    'pidfile=s'         => \$pidfile,
    'logfile=s'         => \$logfile,
) || die('Error parsing options');

sub fixup { return "/tmp/fusetest-" . $ENV{LOGNAME} . shift }

sub x_getattr {
    my ($file) = fixup(shift);
    my (@list) = lstat($file);
    return -$! unless @list;
    return @list;
}

sub x_getdir {
    my ($dirname) = fixup(shift);
    unless(opendir(DIRHANDLE,$dirname)) {
        return -ENOENT();
    }
    my (@files) = readdir(DIRHANDLE);
    closedir(DIRHANDLE);
    return (@files, 0);
}

# FUSE3 uses readdir instead of getdir
sub x_readdir { x_getdir(@_) }

sub x_open {
    my ($file) = fixup(shift);
    my ($mode) = shift;
    return -$! unless sysopen(FILE,$file,$mode);
    close(FILE);
    return 0;
}

sub x_release {
    my ($file) = fixup(shift);
    return 0;
}

sub x_read {
    my ($file,$bufsize,$off) = @_;
    my ($rv) = -ENOSYS();
    my ($handle) = new IO::File;
    return -ENOENT() unless -e ($file = fixup($file));
    my ($fsize) = -s $file;
    return -ENOSYS() unless open($handle,$file);
    if(seek($handle,$off,SEEK_SET)) {
        read($handle,$rv,$bufsize);
    }
    return $rv;
}

sub x_read_buf {
    my ($file, $size, $off, $bufvec) = @_;
    my $rv = 0;
    my ($handle) = new IO::File;
    return -ENOENT() unless -e ($file = fixup($file));
    my ($fsize) = -s $file;
    return -ENOSYS() unless open($handle,$file);
    if(seek($handle,$off,SEEK_SET)) {
        $rv = $bufvec->[0]{'size'} = read($handle,$bufvec->[0]{'mem'},$size);
    }
    return $rv;
}

sub x_write {
    my ($file,$buf,$off) = @_;
    my ($rv);
    return -ENOENT() unless -e ($file = fixup($file));
    my ($fsize) = -s $file;
    return -ENOSYS() unless open(FILE,'+<',$file);
    if($rv = seek(FILE,$off,SEEK_SET)) {
        $rv = print(FILE $buf);
    }
    $rv = -ENOSYS() unless $rv;
    close(FILE);
    return length($buf);
}

sub x_write_buf {
    my ($file,$off,$bufvec) = @_;
    my ($rv);
    return -ENOENT() unless -e ($file = fixup($file));
    my ($fsize) = -s $file;
    return -ENOSYS() unless open(FILE,'+<',$file);
    # If by some chance we get a non-contiguous buffer, or an FD-based
    # buffer (or both!), then copy all of it into one contiguous buffer.
    if ($#$bufvec > 0 || $bufvec->[0]{flags} & &Filesys::Fuse3::FUSE_BUF_IS_FD()) {
        my $single = [ {
                flags   => 0,
                fd      => -1,
                mem     => undef,
                pos     => 0,
                size    => Filesys::Fuse3::fuse_buf_size($bufvec),
        } ];
        Filesys::Fuse3::fuse_buf_copy($single, $bufvec);
        $bufvec = $single;
    }
    if($rv = seek(FILE,$off,SEEK_SET)) {
        $rv = print(FILE $bufvec->[0]{mem});
    }
    $rv = -ENOSYS() unless $rv;
    close(FILE);
    return $rv;
}

sub err { return (-shift || -$!) }

sub x_readlink { return readlink(fixup(shift));         }
sub x_unlink   { return unlink(fixup(shift)) ? 0 : -$!; }

sub x_symlink { print "symlink\n"; return symlink(shift,fixup(shift)) ? 0 : -$!; }

sub x_rename {
    my ($old) = fixup(shift);
    my ($new) = fixup(shift);
    my ($err) = rename($old,$new) ? 0 : -ENOENT();
    return $err;
}
sub x_link { return link(fixup(shift),fixup(shift)) ? 0 : -$! }
sub x_chown {
    my ($fn) = fixup(shift);
    local $!;
    print "nonexistent $fn\n" unless -e $fn;
    my ($uid,$gid) = @_;
    if( $use_lchown ){
		lchown($uid, $gid, $fn);
	}else{
		chown($uid, $gid, $fn);
	}
    return -$!;
}
sub x_chmod {
    my ($fn) = fixup(shift);
    my ($mode) = shift;
    my ($err) = chmod($mode,$fn) ? 0 : -$!;
    return $err;
}
sub x_truncate { return truncate(fixup(shift),shift) ? 0 : -$! ; }
sub x_utime { return utime($_[1],$_[2],fixup($_[0])) ? 0:-$!; }

# FUSE3 uses utimens instead of utime; times arrive as floats (seconds since epoch)
sub x_utimens {
    my ($file, $atime, $mtime) = @_;
    $file = fixup($file);
    return -ENOENT() unless -e $file;
    return utime(int($atime), int($mtime), $file) ? 0 : -$!;
}

sub x_mkdir { my ($name, $perm) = @_; return 0 if mkdir(fixup($name),$perm); return -$!; }
sub x_rmdir { return 0 if rmdir fixup(shift); return -$!; }

sub x_create {
    my ($file, $modes, $flags) = @_;
    printf(STDERR "x_create(): file: \"\%s\"; modes: \%o; flags: \%o\n", $file, $modes, $flags);
    $file = fixup($file);
    open(FILE, '>', $file) || return -$!;
    print FILE '';
    close(FILE);
    chmod S_IMODE($modes), $file;
    return 0;
}

sub x_mknod {
    # since this is called for ALL files, not just devices, I'll do some checks
    # and possibly run the real mknod command.
    my ($file, $modes, $dev) = @_;
    $file = fixup($file);
    undef $!;
    if (S_ISREG($modes)) {
        open(FILE, '>', $file) || return -$!;
        print FILE '';
        close(FILE);
        chmod S_IMODE($modes), $file;
        return 0;
    }
    elsif (S_ISFIFO($modes)) {
        my ($rv) = POSIX::mkfifo($file, S_IMODE($modes));
        return $rv ? 0 : -POSIX::errno();
    }
    elsif (S_ISCHR($modes) || S_ISBLK($modes)) {
        if($has_mknod){
                Unix::Mknod::mknod($file, $modes, $dev);
                return -$!;
        }else{
                return -POSIX::errno();
        }
    }
    # S_ISSOCK maybe should be handled; however, for our test it should
    # not really matter.
    else {
        return -&ENOSYS;
    }
    return -$!;
}

# kludge
sub x_statfs {
    if ($has_Filesys__Statvfs && $use_real_statfs) {
        (my($bsize, $frsize, $blocks, $bfree, $bavail,
            $files, $ffree, $favail, $flag,
            $namemax) = statvfs('/tmp')) || return -$!;
        return ($namemax, $files, $ffree, $blocks, $bavail, $bsize);
    }
    return 255,1000000,500000,1000000,500000,4096;
}

# Required for some edge cases where a simple fork() won't do.
# from http://perldoc.perl.org/perlipc.html#Complete-Dissociation-of-Child    -from-Parent
sub daemonize {
    chdir("/") || die "can't chdir to /: $!";
    open(STDIN, '<', '/dev/null') || die "can't read /dev/null: $!";
    if ($logfile) {
        open(STDOUT, '>', $logfile) || die "can't open logfile: $!";
    }
    else {
        open(STDOUT, '>', '/dev/null') || die "can't write to /dev/null: $!";
    }
    defined(my $pid = fork()) || die "can't fork: $!";
    exit if $pid; # non-zero now means I am the parent
    (setsid() != -1) || die "Can't start a new session: $!";
    open(STDERR, '>&', \*STDOUT) || die "can't dup stdout: $!";
    if ($pidfile) {
        open(PIDFILE, '>', $pidfile);
        print PIDFILE $$, "\n";
        close(PIDFILE);
    }
}

my ($mountpoint) = '';
if (@ARGV){
        $mountpoint = shift(@ARGV)
}
else {
        print <<'_EOT_';

 Usage: loopback.pl <mountpoint> [options]

 Options:
 --debug                Turn on debugging (verbose) output
 --use-threads          Use threads
 --use-real-statfs      Use real stat command against /tmp or generic values
 --pidfile              Create a file at the provided path containing PID
 --logfile              Direct stdout/stderr to file instead of /dev/null

_EOT_
	exit;
}

if (! -d $mountpoint) {
    print STDERR "ERROR: attempted to mount to non-directory\n";
    return -&ENOTDIR
}

daemonize();

Filesys::Fuse3::main(
    'mountpoint'    => $mountpoint,
    'getattr'       => 'main::x_getattr',
    'readlink'      => 'main::x_readlink',
    'readdir'       => 'main::x_readdir',
    'create'        => 'main::x_create',
    'mknod'         => 'main::x_mknod',
    'mkdir'         => 'main::x_mkdir',
    'unlink'        => 'main::x_unlink',
    'rmdir'         => 'main::x_rmdir',
    'symlink'       => 'main::x_symlink',
    'rename'        => 'main::x_rename',
    'link'          => 'main::x_link',
    'chmod'         => 'main::x_chmod',
    'chown'         => 'main::x_chown',
    'truncate'      => 'main::x_truncate',
    'utimens'       => 'main::x_utimens',
    'open'          => 'main::x_open',
    'release'       => 'main::x_release',
    'read'          => 'main::x_read',
    'read_buf'      => 'main::x_read_buf',
    'write'         => 'main::x_write',
    'write_buf'     => 'main::x_write_buf',
    'statfs'        => 'main::x_statfs',
    %extraopts,
);

# vim: ts=4 ai et hls
