#!/usr/bin/perl -w
# (c) Copyright 2001-2008. CodeWeavers, Inc.
use strict;


# Portable which(1) implementation
sub cxwhich($$;$)
{
    my ($dirs, $app, $noexec)=@_;
    if ($app =~ /^\//)
    {
        return $app if ((-x $app or $noexec) and -f $app);
    }
    elsif ($app =~ /\//)
    {
        require Cwd;
        my $path=Cwd::cwd() . "/$app";
        return $path if ((-x $path or $noexec) and -f $path);
    }
    else
    {
        foreach my $dir (split /:/, $dirs)
        {
            return "$dir/$app" if ($dir ne "" and (-x "$dir/$app" or $noexec) and -f "$dir/$app");
        }
    }
    return undef;
}

# Fast dirname() implementation
sub _cxdirname($)
{
    my ($path)=@_;
    return undef if (!defined $path);
    return "." if ($path !~ s!/+[^/]+/*$!!s);
    return "/" if ($path eq "");
    return $path;
}

# Locate where CrossOver is installed by looking for the directory
# where this this script is located, unwinding symlinks on the way
sub locate_cx_root()
{
    if (!defined $ENV{CX_ROOT})
    {
        my $argv0=cxwhich($ENV{PATH},$0);
        $argv0=$0 if (!defined $argv0);
        if ($argv0 !~ m+^/+)
        {
            require Cwd;
            $argv0=Cwd::cwd() . "/$argv0";
        }
        my $dir=_cxdirname($argv0);
        while (!-x "$dir/cxmenu" or !-f "$dir/cxmenu")
        {
            last if (!-l $argv0);
            $argv0=readlink($argv0);
            $argv0="$dir/$argv0" if ($argv0 !~ m+^/+);
            $dir=_cxdirname($argv0);
        }
        $dir =~ s%(/\.)*$%%;
        $dir =~ s%(/\./(\./)*)%/%;
        $ENV{CX_ROOT}=_cxdirname($dir);
    }
    if (!-x "$ENV{CX_ROOT}/bin/cxmenu" or !-f "$ENV{CX_ROOT}/bin/cxmenu")
    {
        my $name0=$0;
        $name0 =~ s+^.*/++;
        print STDERR "$name0:error: could not find CrossOver in '$ENV{CX_ROOT}'\n";
        exit 1;
    }
    return $ENV{CX_ROOT};
}

BEGIN {
    unshift @INC, locate_cx_root() . "/lib/perl";
}
use CXLog;
use CXUtils;


# Process command-line options
my $opt_device;
my $opt_mountpoint;
my $opt_xterm;
my $opt_force;
my $opt_verbose;
my $opt_help;
require CXOpts;
my $cxopts=CXOpts->new();
$cxopts->add_options(["d|device=s"     => \$opt_device,
                      "m|mountpoint=s" => \$opt_mountpoint,
                      "xterm"          => \$opt_xterm,
                      "force"          => \$opt_force,
                      "verbose!"       => \$opt_verbose,
                      "?|h|help"       => \$opt_help
                     ]);
my $err=$cxopts->parse();
CXLog::fdopen(2) if ($opt_verbose);


# Validate the command line options
my $usage;
if ($err)
{
    cxerr("$err\n");
    $usage=2;
}
elsif ($opt_help)
{
    $usage=0;
}
else
{
    if (defined $opt_device and !-e $opt_device)
    {
        cxerr("'$opt_device' does not exist or is not a device\n");
        $usage=2;
    }
    if (defined $opt_mountpoint)
    {
        if (!-d $opt_mountpoint)
        {
            cxerr("'$opt_mountpoint' does not exist or is not a directory\n");
            $usage=2;
        }
        else
        {
            $opt_mountpoint =~ s!/$!!;
            $opt_mountpoint =~ s!/+!/!g;
        }
    }
    if (defined $opt_mountpoint and defined $opt_device)
    {
        cxerr("--device and --mountpoint are mutually exclusive\n");
        $usage=2;
    }
    if ($opt_force and $opt_xterm)
    {
        cxerr("the --xterm and --force options are mutually incompatible\n");
        $usage=2;
    }
}

# Print usage
if (defined $usage)
{
    my $name0=cxname0();
    if ($usage)
    {
        cxerr("try '$name0 --help' for more information\n");
        exit $usage;
    }
    print "Usage: $name0 [--xterm] [--force] [--device DEV] [--mountpoint DIR]\n";
    print "                    [--verbose] [--help]\n";

    print "\n";
    print "Modifies /etc/fstab so CD-ROMs are mounted with the unhide option. This is\n";
    print "needed on Linux kernels < 2.6.13 for compatibility with Windows CDs. Also\n";
    print "makes the CD devices world-readable.\n";

    print "\n";
    print "Options:\n";
    print "  --device DEV Check this device's permissions\n";
    print "  --mountpoint DIR Check the corresponding device's permissions\n";
    print "  --xterm      Runs this command in a fresh terminal emulator\n";
    print "  --force      Don't ask for confirmation before performing the changes\n";
    print "  --verbose    Print more information about what is going on\n";
    print "  --help, -h   Shows this help message\n";
    exit 0;
}

# Recurse inside an xterm
if ($opt_xterm)
{
    my @args=CXUtils::get_terminal_emulator(cxgettext("CrossOver - Fixing '/etc/fstab'"));
    if (!@args)
    {
        cxerr("unable to find a suitable terminal emulator\n");
        exit 1;
    }
    push @args,"-e","$0";
    push @args,"--device",$opt_device if ($opt_device);
    push @args,"--mountpoint",$opt_mountpoint if ($opt_mountpoint);
    cxexec(@args);
    cxerr("unable to run '@args': $!\n");
    exit 1;
}

sub fix_options($$$$)
{
    my ($options, $rfstype, $nounhide_iso, $nouser)=@_;

    # Try to determine where to insert the new options
    my $index=0;
    for (my $i=0; $i<@$options; $i++)
    {
        if ($options->[$i] eq "--")
        {
            $index=$i+1;
            last;
        }
        elsif ($options->[$i] =~ /^(?:fs=|defaults$)/)
        {
            $index=$i+1;
            # but still look for '--'
        }
    }

    # Insert 'unhide' if needed, and always remove 'hide'
    my $ind = 0;
    foreach my $opt (@$options)
    {
        if (!$nounhide_iso and $opt =~ /^unhide$/)
        {
            splice @$options, $ind, 1;
            splice @$options, $index, 0, "unhide";
        }
        elsif ($opt =~ /^hide$/)
        {
            splice @$options, $ind, 1;
        }
        $ind = $ind+1;
    }
    if (!grep /^unhide$/, @$options and (!$nounhide_iso or $rfstype =~ /,udf,/))
    {
        splice @$options, $index, 0, "unhide";
    }

    # Insert 'user' if needed
    if (!$nouser)
    {
        my $ind = 0;
        foreach my $opt (@$options)
        {
           if ($opt =~ s/^(?:no)?(users?)$/$1/)
           {
              splice @$options, $ind, 1;
              splice @$options, $index, 0, $opt;
           }
           $ind = $ind+1;
        }
        splice @$options, $index, 0, "user"   if (!grep /^(?:no)?users?$/, @$options);
    }
}

my $kernel_version=cxbackquote("cat /proc/version 2>/dev/null");
my $nounhide_iso=1;
if ($kernel_version =~ /^Linux version (\d+)\.(\d+)\.(\d+)/)
{
    my ($x, $y, $z)=($1, $2, $3);
    if ($x < 2 or ($x == 2 and $y < 6) or ($x == 2 and $y == 6 and $z < 13))
    {
        $nounhide_iso=0;
    }
}

# Parse /etc/fstab and modify it
my $fh;
if (!open($fh, "<", "/etc/fstab"))
{
    cxerr("unable to open '/etc/fstab' for reading: $!\n");
    exit 1;
}

my @lines;
my %chmods;
my %devices;
my $modified;
foreach my $line (<$fh>)
{
    push @lines, $line;
    next if ($line =~ /^\s*\#/);

    my $entry = $line;
    $entry =~ s/\#.*$//;
    cxlog($entry);
    my @fields = split /\s+/, $entry;
    if (@fields < 4)
    {
        cxlog(" -> skipped, not enough fields\n");
        next;
    }

    # Extract the basic data
    my @options=split /,/, $fields[3];
    my ($nfstype, $rfstype)=CXUtils::parse_fstab_get_fstypes($fields[2], \@options);
    my $mpoint=$fields[1];
    my $device=CXUtils::parse_fstab_get_device($fields[0], $nfstype, \@options);

    # Only fix the one device we've been asked to fix
    if (defined $opt_device and $device ne $opt_device)
    {
        cxlog(" -> skipped, wrong device\n");
        next;
    }
    if (defined $opt_mountpoint and $mpoint ne $opt_mountpoint)
    {
        cxlog(" -> skipped, wrong mount point\n");
        next;
    }

    # Skip non-CD entries
    if ($rfstype =~ /,auto,/ and
        $device !~ /(?:cdr|dvd)/ and $mpoint !~ /(?:cdr|dvd)/)
    {
        cxlog(" -> skipped, auto filesystem but not a CD\n");
        next;
    }
    elsif ($rfstype !~ /,(?:iso9660|udf),/)
    {
        cxlog(" -> skipped, wrong filesystem type ($nfstype/$rfstype)\n");
        next;
    }

    # Add the device for permission fixing
    $chmods{$device}=1 if (!-r $device);

    # Fix the options
    my $nouser=1 if ($nfstype eq ",supermount,");
    fix_options(\@options, $rfstype, $nounhide_iso, $nouser);
    my $opts=join(",", @options);
    next if ($fields[3] eq $opts);
    $devices{$device}=1;

    # Modify the line in-place so that we don't lose any of the formatting
    $lines[-1] =~ s/(\s+)\Q$fields[3]\E(\s*)/$1$opts$2/;
    $modified = 1;
}
close($fh);
if (defined $opt_device)
{
    $chmods{$opt_device}=1 if (!-r $opt_device);
    $devices{$opt_device}=1;
}

my $cmd="";
my $tmpfile;
my $messagestr="";
if ($modified)
{
    $messagestr.=cxgettext("\n- Generate a new fstab file containing the 'unhide' and 'user' options:\n");
    umask(umask() | 0022);
    $tmpfile = "/tmp/fstab.$$";
    my $tmpfh;
    if (!open($tmpfh, ">", $tmpfile))
    {
        cxerr("unable to open '$tmpfile' for writing: $!\n");
        exit 1;
    }
    foreach my $line (@lines)
    {
        print $tmpfh $line;
    }
    close($tmpfh);
    $cmd =  "mv /etc/fstab /etc/fstab.$$ && cat " . shquote_string($tmpfile) . " >/etc/fstab";

    ## really print the file out from the place it was written
    $messagestr.="-----------------------------------------------------------------\n";
    my $in;
    if (!open($in, "<", $tmpfile))
    {
        cxerr("unable to open '$tmpfile' for reading: $!\n");
        exit 1;
    }
    while (<$in>)
    {
        $messagestr.=$_;
    }
    close($in);
    $messagestr.="-----------------------------------------------------------------\n";
    $messagestr.=cxgettext("Your old fstab will be backed up to '\%s'\n","/etc/fstab.$$");
}
if (%devices)
{
    my $remount_list="";
    my $mount_data=cxbackquote("mount 2>&1", 1);
    my $device_re="^(?:" . join("|", map { quotemeta($_) } keys %devices) . ")\$";
    foreach my $line (split /\n/, $mount_data)
    {
        cxlog("$line\n");
        if ($line =~ /^(.*) on (.*) type (\w+) \((.*)\)$/)
        {
            my ($device, $mpoint, $fstype, $opts)=($1, $2, $3, $4);
            my @options=split /,/, $opts;
            if (grep /^user=/, @options)
            {
                # If a user did the mounting, then don't remount it because
                # then he would not be able to unmount it anymore
                cxlog(" -> skipped, user-mounted\n");
                next;
            }

            my $nfstype=",$fstype,";
            my $real_device=CXUtils::parse_fstab_get_device($device, $nfstype, \@options);
            if ($real_device !~ /$device_re/)
            {
                cxlog(" -> skipped, wrong device\n");
                next;
            }

            # Otherwise, fix the mount options (don't add user) and remount
            fix_options(\@options, $nfstype, $nounhide_iso, 1);
            $opts=join(",", @options);
            $cmd.="; " if ($cmd ne "");
            $cmd.="umount '$mpoint' && mount -t '$fstype' -o '$opts' '$device' '$mpoint'";
            $remount_list.="  $device on $mpoint ($opts)\n";
        }
    }
    if ($remount_list)
    {
        $messagestr.=cxgettext("\n- Remount the following filesystems with the corrected options:\n") . $remount_list;
        $modified=1;
    }
}

if (!$modified and !%chmods)
{
    if (!$opt_force)
    {
        cxmessage(
            "-title", "Fix mount options",
            "No modification is necessary.");
    }
    exit 0;
}

if (%chmods)
{
    my $devices=join(" ", sort keys %chmods);
    $messagestr.=cxgettext("\n- Make the following devices world readable:\n");
    $messagestr.="  $devices\n";
    $cmd .= "; " if ($cmd);
    $cmd .= "chmod ugo+r $devices";
}

if (!$opt_force)
{
    $messagestr=cxgettext("CrossOver needs to:\%s\nDo you want to make the above modifications?", $messagestr);

    my $answer=cxmessage(
        "-title", "Fix mount options",
        "-buttons", "Yes:0,No:1",
        "-default", "Yes",
        "\%s", $messagestr);
    exit 0 if ($answer != 0);
}

CXUtils::cxsu($cmd);
unlink $tmpfile if (defined $tmpfile);
exit 0;
