#!/usr/bin/env perl

# fioc.pl: A Perl conversion of the fioc example IOCTL server program
# from the FUSE distribution. I've endeavored to stay pretty close
# structure-wise to the C version, while using Perl-specific features.
# I wrote this to provide a way to verify my ioctl() wrapper
# implementation would work properly. So far, it seems to, and it will
# interoperate with the C client as well.

use strict;
no strict qw(refs);

use threads;
use threads::shared;

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

use Fuse qw(:all);
use Fcntl qw(:mode);
use POSIX;

my $fioc_size :shared = 0;
use constant FIOC_NAME => 'fioc';
my $fioc_buf :shared = '';
use constant FIOC_NONE  => 0;
use constant FIOC_ROOT  => 1;
use constant FIOC_FILE  => 2;

if ($^O eq 'linux') {
    require 'linux/ioctl.ph';
}
else {
    require 'sys/ioccom.ph';
}

our %sizeof = ('size_t' => length(pack('L!')));
sub FIOC_GET_SIZE { _IOR(ord 'E', 0, 'size_t'); }
sub FIOC_SET_SIZE { _IOW(ord 'E', 1, 'size_t'); }
sub TCGETS { 0x5401; }

sub fioc_resize {
    my ($size) = @_;
    print 'called ', (caller(0))[3], "\n";
    return 0 if $size == $fioc_size;

    if ($size < $fioc_size) {
        $fioc_buf = substr($fioc_buf, 0, $size);
    }
    else {
        $fioc_buf .= "\0" x ($size - $fioc_size);
    }
    $fioc_size = $size;
    return 0;
}

sub fioc_expand {
    my ($size) = @_;
    print 'called ', (caller(0))[3], "\n";
    if ($size > $fioc_size) {
        return fioc_resize($size);
    }
    return 0;
}

sub fioc_file_type {
    my ($path) = @_;
    print 'called ', (caller(0))[3], "\n";
    return FIOC_ROOT if $path eq '/';
    return FIOC_FILE if $path eq '/' . FIOC_NAME;
    return FIOC_NONE;
}

sub fioc_getattr {
    my ($path) = @_;
    print 'called ', (caller(0))[3], "\n";
    my @stbuf = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);

    $stbuf[4] = $<;
    $stbuf[5] = (split(/\s+/, $())[0];
    $stbuf[8] = $stbuf[9] = time();

    my $type = fioc_file_type($path);
    if ($type == FIOC_ROOT) {
        $stbuf[2] = S_IFDIR | 0755;
        $stbuf[3] = 2;
    }
    elsif ($type == FIOC_FILE) {
        $stbuf[2] = S_IFREG | 0644;
        $stbuf[3] = 1;
        $stbuf[7] = $fioc_size;
    }
    else {
        return -&ENOENT;
    }
    return @stbuf;
}

sub fioc_open {
    my ($path, $flags, $info) = @_;
    print 'called ', (caller(0))[3], "\n";

    return 0 if fioc_file_type($path) != FIOC_NONE;
    return -&ENOENT;
}

sub fioc_read {
    my ($path, $size, $offset) = @_;
    print 'called ', (caller(0))[3], "\n";

    return -&EINVAL if fioc_file_type($path) != FIOC_FILE;
    return q{} if $offset > $fioc_size;

    if ($size > $fioc_size - $offset) {
        $size - $fioc_size - $offset;
    }

    return substr($fioc_buf, $offset, $size);
}

sub fioc_write {
    my ($path, $data, $offset) = @_;
    print 'called ', (caller(0))[3], "\n";
    lock($fioc_buf);

    return -&EINVAL if fioc_file_type($path) != FIOC_FILE;
    return -&ENOMEM if fioc_expand($offset + length($data));

    substr($fioc_buf, $offset, length($data), $data);
    return length($data);
}

sub fioc_truncate {
    my ($path, $size) = @_;
    print 'called ', (caller(0))[3], "\n";
    lock($fioc_buf);

    return -&EINVAL if fioc_file_type($path) != FIOC_FILE;

    return fioc_resize($size);
}

sub fioc_readdir {
    my ($path, $offset) = @_;
    print 'called ', (caller(0))[3], "\n";

    return -&EINVAL if fioc_file_type($path) != FIOC_ROOT;

    return ('.', '..', FIOC_NAME, 0);
}

sub fioc_ioctl {
    my ($path, $cmd, $flags, $data) = @_;
    print 'called ', (caller(0))[3], "\n";

    return -&EINVAL if fioc_file_type($path) != FIOC_FILE;
    return -&ENOSYS if $flags & FUSE_IOCTL_COMPAT;

    if ($cmd == FIOC_GET_SIZE) {
        return(0, pack('L!', $fioc_size));
    }
    elsif ($cmd == FIOC_SET_SIZE) {
        lock($fioc_buf);
        fioc_resize(unpack('L!', $data));
        return 0;
    }
    elsif ($cmd == TCGETS) {
        # perl sends TCGETS as part of calling isatty() on opening a file;
        # this appears to be a more canonical answer
        return -&ENOTTY;
    }

    return -&EINVAL;
}

croak("Fuse doesn't have ioctl") unless Fuse::fuse_version() >= 2.8;

Fuse::main(
    'mountpoint' => $ARGV[0],
    'getattr'   => 'main::fioc_getattr',
    'readdir'   => 'main::fioc_readdir',
    'truncate'  => 'main::fioc_truncate',
    'open'      => 'main::fioc_open',
    'read'      => 'main::fioc_read',
    'write'     => 'main::fioc_write',
    'ioctl'     => 'main::fioc_ioctl',
    'threaded'  => 1);
