The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Udev::FFI::Functions;

use strict;
use warnings;

our (@ISA, @EXPORT_OK, %EXPORT_TAGS);

require Exporter;
@ISA = qw(Exporter);

use IPC::Cmd qw(can_run);

use FFI::Platypus;
use FFI::CheckLib;


use constant {
    UDEVADM_LOCATIONS => [
        '/bin/udevadm',
        '/sbin/udevadm'
    ]
};



my $FUNCTIONS = {
    # struct udev *udev_new(void);
    'udev_new' => {
        ffi_data => [ [], 'opaque' ]
    },

    # struct udev *udev_ref(struct udev *udev);
    'udev_ref' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # struct udev *udev_unref(struct udev *udev);
    'udev_unref' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # access to libudev generated lists ====================================

    # struct udev_list_entry *udev_list_entry_get_next(struct udev_list_entry *list_entry);
    'udev_list_entry_get_next' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # struct udev_list_entry *udev_list_entry_get_by_name(struct udev_list_entry *list_entry, const char *name);
    'udev_list_entry_get_by_name' => {
        ffi_data => [ ['opaque', 'string'], 'opaque']
    },

    # const char *udev_list_entry_get_name(struct udev_list_entry *list_entry);
    'udev_list_entry_get_name' => {
        ffi_data => [ ['opaque'], 'string' ]
    },

    # const char *udev_list_entry_get_value(struct udev_list_entry *list_entry);
    'udev_list_entry_get_value' => {
        ffi_data => [ ['opaque'], 'string' ]
    },


    # udev_device ==========================================================

    # struct udev_device *udev_device_ref(struct udev_device *udev_device);
    'udev_device_ref' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # struct udev_device *udev_device_unref(struct udev_device *udev_device);
    'udev_device_unref' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # struct udev *udev_device_get_udev(struct udev_device *udev_device);
    'udev_device_get_udev' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # struct udev_device *udev_device_new_from_syspath(struct udev *udev, const char *syspath);
    'udev_device_new_from_syspath' => {
        ffi_data => [ ['opaque', 'string'], 'opaque' ]
    },

    # struct udev_device *udev_device_new_from_devnum(struct udev *udev, char type, dev_t devnum);
    'udev_device_new_from_devnum' => {
        ffi_data => [ ['opaque', 'signed char', 'uint64_t'], 'opaque' ]
    },

    # struct udev_device *udev_device_new_from_subsystem_sysname(struct udev *udev, const char *subsystem, const char *sysname);
    'udev_device_new_from_subsystem_sysname' => {
        ffi_data => [ ['opaque', 'string', 'string'], 'opaque' ]
    },

    # struct udev_device *udev_device_new_from_device_id(struct udev *udev, const char *id);
    'udev_device_new_from_device_id' => {
        ffi_data => [ ['opaque', 'string'], 'opaque' ],
        since    => 189
    },

    # struct udev_device *udev_device_new_from_environment(struct udev *udev);
    'udev_device_new_from_environment' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # struct udev_device *udev_device_get_parent(struct udev_device *udev_device);
    'udev_device_get_parent' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # struct udev_device *udev_device_get_parent_with_subsystem_devtype(struct udev_device *udev_device,
    #     const char *subsystem, const char *devtype);
    'udev_device_get_parent_with_subsystem_devtype' => {
        ffi_data => [ ['opaque', 'string', 'string'], 'opaque' ]
    },

    # retrieve device properties

    # const char *udev_device_get_devpath(struct udev_device *udev_device);
    'udev_device_get_devpath'  => {
        ffi_data => [ ['opaque'], 'string' ]
    },

    # const char *udev_device_get_subsystem(struct udev_device *udev_device);
    'udev_device_get_subsystem' => {
        ffi_data => [ ['opaque'], 'string' ]
    },

    # const char *udev_device_get_devtype(struct udev_device *udev_device);
    'udev_device_get_devtype' => {
        ffi_data => [ ['opaque'], 'string' ]
    },

    # const char *udev_device_get_syspath(struct udev_device *udev_device);
    'udev_device_get_syspath' => {
        ffi_data => [ ['opaque'], 'string' ]
    },

    # const char *udev_device_get_sysname(struct udev_device *udev_device);
    'udev_device_get_sysname' => {
        ffi_data => [ ['opaque'], 'string' ]
    },

    # const char *udev_device_get_sysnum(struct udev_device *udev_device);
    'udev_device_get_sysnum' => {
        ffi_data => [ ['opaque'], 'string' ]
    },

    # const char *udev_device_get_devnode(struct udev_device *udev_device);
    'udev_device_get_devnode' => {
        ffi_data => [ ['opaque'], 'string' ]
    },

    #int udev_device_get_is_initialized(struct udev_device *udev_device);
    'udev_device_get_is_initialized' => {
        ffi_data => [ ['opaque'], 'int' ]
    },

    # struct udev_list_entry *udev_device_get_devlinks_list_entry(struct udev_device *udev_device);
    'udev_device_get_devlinks_list_entry' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # struct udev_list_entry *udev_device_get_properties_list_entry(struct udev_device *udev_device);
    'udev_device_get_properties_list_entry' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # struct udev_list_entry *udev_device_get_tags_list_entry(struct udev_device *udev_device);
    'udev_device_get_tags_list_entry' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # struct udev_list_entry *udev_device_get_sysattr_list_entry(struct udev_device *udev_device);
    'udev_device_get_sysattr_list_entry' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    #const char *udev_device_get_property_value(struct udev_device *udev_device, const char *key);
    'udev_device_get_property_value' => {
        ffi_data => [ ['opaque', 'string'], 'string' ]
    },

    #const char *udev_device_get_driver(struct udev_device *udev_device);
    'udev_device_get_driver' => {
        ffi_data => [ ['opaque'], 'string' ]
    },

    # dev_t udev_device_get_devnum(struct udev_device *udev_device);
    'udev_device_get_devnum' => {
        ffi_data => [ ['opaque'], 'uint64_t' ]
    },

    #const char *udev_device_get_action(struct udev_device *udev_device);
    'udev_device_get_action' => {
        ffi_data => [ ['opaque'], 'string' ]
    },

    #unsigned long long int udev_device_get_seqnum(struct udev_device *udev_device);
    'udev_device_get_seqnum' => {
        ffi_data => [ ['opaque'], 'unsigned long long' ]
    },

    #unsigned long long int udev_device_get_usec_since_initialized(struct udev_device *udev_device);
    'udev_device_get_usec_since_initialized' => {
        ffi_data => [ ['opaque'], 'unsigned long long' ]
    },

    #const char *udev_device_get_sysattr_value(struct udev_device *udev_device, const char *sysattr);
    'udev_device_get_sysattr_value' => {
        ffi_data => [ ['opaque', 'string'], 'string' ]
    },

    #int udev_device_set_sysattr_value(struct udev_device *udev_device, const char *sysattr, char *value);
    'udev_device_set_sysattr_value' => {
        ffi_data => [ ['opaque', 'string', 'string'], 'int' ],
        since    => 199
    },

    #int udev_device_has_tag(struct udev_device *udev_device, const char *tag);
    'udev_device_has_tag' => {
        ffi_data => [ ['opaque', 'string'], 'int' ]
    },


    # udev_monitor =========================================================

    # access to kernel uevents and udev events

    # struct udev_monitor *udev_monitor_ref(struct udev_monitor *udev_monitor);
    'udev_monitor_ref' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # struct udev_monitor *udev_monitor_unref(struct udev_monitor *udev_monitor);
    'udev_monitor_unref' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # struct udev *udev_monitor_get_udev(struct udev_monitor *udev_monitor);
    'udev_monitor_get_udev' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    #kernel and udev generated events over netlink

    # struct udev_monitor *udev_monitor_new_from_netlink(struct udev *udev, const char *name);
    'udev_monitor_new_from_netlink' => {
        ffi_data => [ ['opaque', 'string'], 'opaque' ]
    },

    # bind socket

    # int udev_monitor_enable_receiving(struct udev_monitor *udev_monitor);
    'udev_monitor_enable_receiving' => {
        ffi_data => [ ['opaque'], 'int' ]
    },

    # int udev_monitor_set_receive_buffer_size(struct udev_monitor *udev_monitor, int size);
    'udev_monitor_set_receive_buffer_size' => {
        ffi_data => [ ['opaque', 'int'], 'int' ]
    },

    # int udev_monitor_get_fd(struct udev_monitor *udev_monitor);
    'udev_monitor_get_fd' => {
        ffi_data => [ ['opaque'], 'int' ]
    },

    # struct udev_device *udev_monitor_receive_device(struct udev_monitor *udev_monitor);
    'udev_monitor_receive_device' => {
        ffi_data => [ ['opaque'], 'opaque']
    },

    # n-kernel socket filters to select messages that get delivered to a listener

    # int udev_monitor_filter_add_match_subsystem_devtype(struct udev_monitor *udev_monitor,
    #     const char *subsystem, const char *devtype);
    'udev_monitor_filter_add_match_subsystem_devtype' => {
        ffi_data => [ ['opaque', 'string', 'string'], 'int' ]
    },

    # int udev_monitor_filter_add_match_tag(struct udev_monitor *udev_monitor, const char *tag);
    'udev_monitor_filter_add_match_tag' => {
        ffi_data => [ ['opaque', 'string'], 'int' ]
    },

    # int udev_monitor_filter_update(struct udev_monitor *udev_monitor);
    'udev_monitor_filter_update' => {
        ffi_data => [ ['opaque'], 'int' ]
    },

    # int udev_monitor_filter_remove(struct udev_monitor *udev_monitor);
    'udev_monitor_filter_remove' => {
        ffi_data => [ ['opaque'], 'int' ]
    },


    # udev_enumerate =======================================================

    # search sysfs for specific devices and provide a sorted list

    # struct udev_enumerate *udev_enumerate_ref(struct udev_enumerate *udev_enumerate);
    'udev_enumerate_ref' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # struct udev_enumerate *udev_enumerate_unref(struct udev_enumerate *udev_enumerate);
    'udev_enumerate_unref' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # struct udev *udev_enumerate_get_udev(struct udev_enumerate *udev_enumerate);
    'udev_enumerate_get_udev' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # struct udev_enumerate *udev_enumerate_new(struct udev *udev);
    'udev_enumerate_new' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # device properties filter

    # int udev_enumerate_add_match_subsystem(struct udev_enumerate *udev_enumerate, const char *subsystem);
    'udev_enumerate_add_match_subsystem' => {
        ffi_data => [ ['opaque', 'string'], 'int' ]
    },

    # int udev_enumerate_add_nomatch_subsystem(struct udev_enumerate *udev_enumerate, const char *subsystem);
    'udev_enumerate_add_nomatch_subsystem' => {
        ffi_data => [ ['opaque', 'string'], 'int' ]
    },

    # int udev_enumerate_add_match_sysattr(struct udev_enumerate *udev_enumerate, const char *sysattr, const char *value);
    'udev_enumerate_add_match_sysattr' => {
        ffi_data => [ ['opaque', 'string', 'string'], 'int' ]
    },

    # int udev_enumerate_add_nomatch_sysattr(struct udev_enumerate *udev_enumerate, const char *sysattr, const char *value);
    'udev_enumerate_add_nomatch_sysattr' => {
        ffi_data => [ ['opaque', 'string', 'string'], 'int' ]
    },

    # int udev_enumerate_add_match_property(struct udev_enumerate *udev_enumerate, const char *property, const char *value);
    'udev_enumerate_add_match_property' => {
        ffi_data => [ ['opaque', 'string', 'string'], 'int' ]
    },

    # int udev_enumerate_add_match_sysname(struct udev_enumerate *udev_enumerate, const char *sysname);
    'udev_enumerate_add_match_sysname' => {
        ffi_data => [ ['opaque', 'string'], 'int' ]
    },

    # int udev_enumerate_add_match_tag(struct udev_enumerate *udev_enumerate, const char *tag);
    'udev_enumerate_add_match_tag' => {
        ffi_data => [ ['opaque', 'string'], 'int' ]
    },

    # int udev_enumerate_add_match_parent(struct udev_enumerate *udev_enumerate, struct udev_device *parent);
    'udev_enumerate_add_match_parent' => {
        ffi_data => [ ['opaque', 'opaque'], 'int' ]
    },

    # int udev_enumerate_add_match_is_initialized(struct udev_enumerate *udev_enumerate);
    'udev_enumerate_add_match_is_initialized' => {
        ffi_data => [ ['opaque'], 'int' ]
    },

    # int udev_enumerate_add_syspath(struct udev_enumerate *udev_enumerate, const char *syspath);
    'udev_enumerate_add_syspath' => {
        ffi_data => [ ['opaque', 'string'], 'int' ]
    },

    # run enumeration with active filters

    # int udev_enumerate_scan_devices(struct udev_enumerate *udev_enumerate);
    'udev_enumerate_scan_devices' => {
        ffi_data => [ ['opaque'], 'int' ]
    },

    # int udev_enumerate_scan_subsystems(struct udev_enumerate *udev_enumerate);
    'udev_enumerate_scan_subsystems' => {
        ffi_data => [ ['opaque'], 'int' ]
    },

    # return device list

    # struct udev_list_entry *udev_enumerate_get_list_entry(struct udev_enumerate *udev_enumerate);
    'udev_enumerate_get_list_entry' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    }
};



@EXPORT_OK = ( keys(%$FUNCTIONS), qw(get_entries));

%EXPORT_TAGS = (
    'all' => \@EXPORT_OK
);


my $init = 0;



sub udev_version {
    my $full_path = can_run('udevadm');

    if(!defined $full_path) {
        for(@{ +UDEVADM_LOCATIONS }) {
            if(-f) {
                $full_path = $_;
                last;
            }
        }
    }

    if(!defined $full_path) {
        $@ = "Can't find `udevadm` utility";
        return undef;
    }


    {
        local $SIG{__WARN__} = sub {}; # silence shell output if error

        if(open my $ph, '-|', $full_path, '--version') {
            my $out = <$ph>;

            if(defined($out) && $out =~ /^(\d+)\s*$/) {
                return $1;
            }

            $@ = "Can't get udev version from `udevadm` utility";
            return undef;
        }
    }

    $@ = "Can't run `udevadm` utility";
    return undef;
}



my $_function_not_attach = sub {
    my $udev_version = udev_version();

    die "Function '".$_[0]."' not attached from udev library\n".
        "`udevadm` version: ".(defined($udev_version) ?$udev_version :'unknown')."\n";
};



sub get_entries {
    my $entry = shift;

    if(wantarray) {
        my @a = ();

        while(defined($entry)) {
            push @a, udev_list_entry_get_name($entry);
            $entry = udev_list_entry_get_next($entry);
        }

        return @a;
    }


    my %h = ();

    while(defined($entry)) {
        $h{ udev_list_entry_get_name($entry) } = udev_list_entry_get_value($entry);
        $entry = udev_list_entry_get_next($entry);
    }

    return \%h;
}



sub init {
    return 1 if $init;


    my ($libudev) = find_lib(
        lib => 'udev'
    );
    if(!$libudev) {
        $@ = "Can't find udev library";
        return 0;
    }

    my $udev_version = udev_version() || 0;

    my $ffi = FFI::Platypus->new;
    $ffi->lib($libudev);

    if(8 != (my $sizeof_dev_t = $ffi->sizeof('dev_t'))) {
        $@ = "sizeof(dev_t) != 8 on your OS (sizeof(dev_t) == $sizeof_dev_t). Please report this to the author";
        return 0;
    }

    for my $funct (keys %$FUNCTIONS) {
        eval {
            # attach locks the function and the FFI::Platypus instance into memory permanently,
            # since there is no way to deallocate an xsub
            $ffi->attach($funct => $FUNCTIONS->{$funct}{ffi_data}[0] => $FUNCTIONS->{$funct}{ffi_data}[1]);
        };
        if($@) {
            if(!exists($FUNCTIONS->{$funct}{since}) || $udev_version >= $FUNCTIONS->{$funct}{since}) {
                $@ = $1
                    if $@ =~ m{^(.*)\s+at\s.*line\s\d+.}xms;

                $@ = "Can't attach '$funct' function from udev library: $@";
                return 0;
            }

            no strict 'refs';
            *$funct = sub { $_function_not_attach->($funct) };
        }

        # function attached
        delete $FUNCTIONS->{$funct};
    }

    return ++$init;
}



1;