# Copyright (c) 2012, cPanel, Inc.
# All rights reserved.
# http://cpanel.net/
#
# This is free software; you can redistribute it and/or modify it under the same
# terms as Perl itself.  See the LICENSE file for further details.

package Filesys::POSIX::VFS;

use strict;
use warnings;

use Filesys::POSIX::Bits;
use Filesys::POSIX::Path       ();
use Filesys::POSIX::VFS::Inode ();

use Carp qw/confess/;

sub new {
    return bless {
        'mounts'  => [],
        'devices' => {},
        'vnodes'  => {}
      },
      shift;
}

sub statfs {
    my ( $self, $start, %opts ) = @_;
    my $inode = $start;
    my $ret;

    while ( $inode->{'vnode'} ) {
        $inode = $inode->{'vnode'};
    }

    if ( $opts{'exact'} ) {
        $ret = $self->{'vnodes'}->{$inode};
    }
    else {
        $ret = $self->{'devices'}->{ $inode->{'dev'} };
    }

    unless ($ret) {
        confess('Not mounted') unless $opts{'silent'};
    }

    return $ret;
}

sub mountlist {
    my ($self) = @_;
    return @{ $self->{'mounts'} };
}

#
# It should be noted that any usage of pathnames in this module are entirely
# symbolic and are not used for canonical purposes.  The higher-level
# filesystem layer should take on the responsibility of providing both the
# canonically-correct absolute pathnames for mount points, and helping locate
# the appropriate VFS mount point for querying purposes.
#
sub mount {
    my ( $self, $dev, $path, $mountpoint, %data ) = @_;

    if ( grep { $_->{'dev'} eq $dev } @{ $self->{'mounts'} } ) {
        confess('Already mounted');
    }

    $data{'special'} ||= scalar $dev;

    #
    # Generate a generic BSD-style filesystem type string.
    #
    my $type = lc ref $dev;
    $type =~ s/^([a-z_][a-z0-9_]*::)*//;

    #
    # Create a vnode record munged from the mountpoint and new
    # filesystem root.
    #
    my $vnode = Filesys::POSIX::VFS::Inode->new( $mountpoint, $dev->{'root'} );

    #
    # Associate the mountpoint and filesystem roots with this vnode.
    #
    $mountpoint->{'vnode'} = $vnode;
    $dev->{'root'}->{'vnode'} = $vnode;

    #
    # Generate the mount record.
    #
    my $mount = {
        'mountpoint' => $mountpoint,
        'root'       => $dev->{'root'},
        'special'    => $data{'special'},
        'dev'        => $dev,
        'type'       => $type,
        'path'       => $path,
        'vnode'      => $vnode,

        'flags' => { map { $_ => $data{$_} } grep { $_ ne 'special' } keys %data }
    };

    #
    # Store the mount record in the ordered mount list.
    #
    push @{ $self->{'mounts'} }, $mount;

    #
    # Associate the vnode with the mount rcord.
    #
    $self->{'vnodes'}->{$vnode} = $mount;

    #
    # Finally, associate the filesystem with the mount record.
    #
    $self->{'devices'}->{$dev} = $mount;

    return $self;
}

sub vnode {
    my ( $self, $start ) = @_;
    my $inode = $start;

    return undef unless $inode;

    while ( $inode->{'vnode'} ) {
        $inode = $inode->{'vnode'};
    }

    my $mount = $self->{'devices'}->{ $inode->{'dev'} };

    if ( $mount->{'flags'}->{'noexec'} ) {
        $inode->{'mode'} &= ~$S_IX;
    }

    if ( $mount->{'flags'}->{'nosuid'} ) {
        $inode->{'mode'} &= ~$S_ISUID;
    }

    foreach (qw/uid gid/) {
        if ( defined $mount->{'flags'}->{$_} ) {
            $inode->{$_} = $mount->{'flags'}->{$_};
        }
    }

    return $inode;
}

sub unmount {
    my ( $self, $mount ) = @_;

    #
    # First, check to see that the filesystem mount record found is a
    # dependency for another mounted filesystem.
    #
    foreach ( @{ $self->{'mounts'} } ) {
        next if $_ == $mount;
        confess('Device or resource busy') if $_->{'mountpoint'}->{'dev'} == $mount->{'dev'};
    }

    #
    # Pluck the filesystem from the mount list.
    #
    for ( my $i = 0; $self->{'mounts'}->[$i]; $i++ ) {
        next unless $self->{'mounts'}->[$i] eq $mount;
        splice @{ $self->{'mounts'} }, $i;
        last;
    }

    #
    # Untie the vnode reference from its original mount point and root.
    #
    delete $mount->{'mountpoint'}->{'vnode'};
    delete $mount->{'root'}->{'vnode'};

    #
    # Break references to the mount record from the per-vnode hash.
    #
    delete $self->{'vnodes'}->{ $mount->{'vnode'} };

    #
    # Kill references to the mount record from the per-device hash.
    #
    delete $self->{'devices'}->{ $mount->{'dev'} };

    return $self;
}

1;
