Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
crossover / opt / cxoffice / bin / cxassoc
Size: Mime:
#!/usr/bin/perl
# (c) Copyright 2005-2012, 2014. CodeWeavers, Inc.
use warnings;
use strict;

my @desktop_plugins=("CXAssocWindows", "CXAssocCheck",
                     "CXAssocMailcap", "CXAssocDebian", "CXAssocCDE",
                     "CXAssocXDG", "CXAssocMacOSX");


# 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 the cxmenu script is located, unwinding symlinks on the way
sub locate_cx_root(;$)
{
    my ($fallback)=@_;
    my $argv0=cxwhich($ENV{PATH},$0);
    $argv0=$0 if (!defined $argv0);
    if ($argv0 !~ m+^/+)
    {
        require Cwd;
        $argv0=Cwd::cwd() . "/$argv0";
    }
    my $dir=_cxdirname($argv0);
    my $bindir=$dir;
    $bindir =~ s%/lib$%/bin%;
    while (!-x "$bindir/cxmenu" or !-f "$bindir/cxmenu")
    {
        last if (!-l $argv0);
        $argv0=readlink($argv0);
        $argv0="$dir/$argv0" if ($argv0 !~ m+^/+);
        $dir=_cxdirname($argv0);
        $bindir=$dir;
        $bindir =~ s%/lib$%/bin%;
    }
    $bindir =~ s%/(?:\./)+%/%g;
    $bindir =~ s%/\.$%%;
    $ENV{CX_ROOT}=_cxdirname($bindir);
    if ((!-x "$ENV{CX_ROOT}/bin/cxmenu" or !-f "$ENV{CX_ROOT}/bin/cxmenu") and
        $fallback)
    {
        $ENV{CX_ROOT}=$fallback;
    }
    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;
use CXAssoc;

my $filters;
sub get_filter_list($$)
{
    my ($cxassoc, $filter)=@_;

    $filter=lc($filter || "");
    my $filter_list=$filters->{$filter};
    if (!defined $filter_list)
    {
        my @list;
        if ($filter eq "")
        {
            @list=$cxassoc->get_section_names();
        }
        else
        {
            @list=split /:+/, $filter;
        }
        $filters->{$filter}=$filter_list=\@list;
    }
    return $filter_list;
}

sub get_install_mode($$$)
{
    my ($eassocid, $hash_mode, $default_mode)=@_;
    foreach my $mode ("default", "alternative", "mime", "ignore")
    {
        if (defined $hash_mode->{$mode} and
            $eassocid =~ /^(?:$hash_mode->{$mode})$/i)
        {
            return $mode;
        }
    }
    return $default_mode;
}

sub update_field($$$)
{
    my ($section, $name, $value)=@_;

    $value||="";
    my $oldval=$section->get($name, "");
    return 0 if ($oldval eq $value);

    if ($value eq "")
    {
        $section->remove($name);
    }
    else
    {
        $section->set($name, $value);
    }
    return 1;
}

sub normalize_eassocid($)
{
    my ($eassocid)=@_;
    $eassocid =~ tr/A-Z/a-z/;
    return map { mangle_string(demangle_string($_)) } split "/", $eassocid;
}

sub valid_mimetype($)
{
    my ($mimetype)=@_;
    my @parts=split m%/%, $mimetype;
    return (@parts == 2);
}

sub maybe_capitalize($)
{
    my ($str)=@_;
    return ucfirst($str) if ($str !~ /[A-Z]/);
    return $str;
}

my %mode_to_score=(
    default     => 0,
    alternative => 1,
    mime        => 2,
    ignore      => 3
);

sub build_assoc_db($)
{
    my ($cxassoc)=@_;

    my $std_verb_names=CXAssoc::std_verb_names();
    my ($massocs, $eassocs, $winmimes, $winexts, %extmimes);
    foreach my $eassocid ($cxassoc->get_section_names())
    {
        my $section=$cxassoc->get_section($eassocid);
        my @parts=split "/", $eassocid;
        if (@parts > 3)
        {
            cxwarn("too many parts in '$eassocid'\n");
            next;
        }
        my $ext=demangle_string($parts[0] || "");
        if (!$ext or $ext !~ s/^\.// or $ext =~ m%[/\\]%)
        {
            cxwarn("invalid extension for association '$eassocid'\n");
            next;
        }
        # Don't demangle $appid and $verb just yet
        my $appid=$parts[1] || "";
        my $verb=$parts[2] || "";
        cxlog("$eassocid -> [$ext | $appid | $verb]\n");

        # Validate and normalise MimeType, Mode and Type
        my $mimetype=$section->get("MimeType", "");
        if ($mimetype ne "" and !valid_mimetype($mimetype))
        {
            cxwarn("invalid MIME type '$mimetype' for association '$eassocid'\n");
            next;
        }
        if (!defined $extmimes{$ext})
        {
            $extmimes{$ext}=$mimetype;
        }
        elsif ($extmimes{$ext} ne $mimetype)
        {
            cxwarn("the '$ext' extension has inconsistent MIME types. Using '$extmimes{$ext}' instead of '$mimetype'\n");
            $mimetype=$extmimes{$ext};
        }

        my $mode=$section->get("Mode", "ignore");
        $mode =~ tr/A-Z/a-z/;
        $mode =~ s/^alternate$/alternative/i;
        if ($mode !~ /^(?:default|alternative|mime|ignore)$/)
        {
            cxwarn("unknown Mode '$mode' for '$eassocid'\n");
            $mode="ignore";
        }
        my $type=$section->get("Type", "raw");
        $type =~ tr/A-Z/a-z/;

        # Create the EAssoc object
        my $eassoc={
            id          => $eassocid,
            ext         => $ext,
            appid       => $appid,
            verb        => $verb,
            verbname    => $section->get("VerbName") ||
                           $std_verb_names->{$verb} ||
                           maybe_capitalize(demangle_string($verb)),
            stdverbname => 0,
            mode        => $mode,

            # Stored here so we can later populate the MAssoc and Mime objects
            localize    => $section->get("Localize"),
            description => $section->get("Description", ""),
            infotip     => $section->get("InfoTip", ""),
            appname     => $section->get("AppName", ""),
            type        => $type,
            icon        => expand_string($section->get("Icon", "")),
            command     => expand_cmdline($section->get("Command", ""))
        };
        my $stdname=$std_verb_names->{$verb};
        if ($eassoc->{verbname} and $stdname and
            ($eassoc->{verbname} eq $stdname or
             $eassoc->{verbname} eq CXAssoc::remove_accelerators($stdname)))
        {
            # Tag the verb so we can try to localize it
            $eassoc->{stdverbname}=1;
        }
        $eassocs->{$eassocid}=$eassoc;

        # Then the 'Extension MIME type'
        my $emimetype="application/x-crossover-$ext";
        my $emime=$winmimes->{$emimetype};
        if (!defined $emime)
        {
            $emime={
                mimetype    => $emimetype,
                exts        => { $ext => 1 },
                real        => 0
            };
            $winmimes->{$emimetype}=$emime;
        }
        $emime->{eassocs}->{$eassocid}=$eassoc;
        $eassoc->{emime}=$emime;

        # The real MIME type if any
        my $mime;
        if ($mimetype)
        {
            $mime=$winmimes->{$mimetype};
            if (!defined $mime)
            {
                $mime={
                    mimetype    => $mimetype,
                    real        => 1
                };
                $winmimes->{$mimetype}=$mime;
            }
            $mime->{exts}->{$ext}=1;
            $mime->{eassocs}->{$eassocid}=$eassoc;
        }

        # Link the extension to the corresponding MIME type
        $winexts->{$ext}=$mime || $emime;

        # And finally the MAssoc object
        my $massocid=mangle_string($mimetype || $emimetype);
        $massocid.=":$appid" if ($appid or $verb);
        $massocid.=":$verb" if ($verb);
        my $massoc=$massocs->{$massocid};
        if (!defined $massoc)
        {
            $massoc={
                id          => $massocid,
                appid       => $appid,
                verb        => $verb,
                mime        => $mime || $emime
            };
            $massocs->{$massocid}=$massoc;
        }
        # Extra MIME types are simply put all together
        foreach my $extramime (split /:+/, $section->get("ExtraMimeTypes", ""))
        {
            if (!valid_mimetype($extramime))
            {
                cxwarn("ignoring invalid extra MIME type $extramime\n");
                next;
            }
            $massoc->{extramimes}->{$extramime}=1;
        }
        $massoc->{eassocs}->{$ext}=$eassoc;
        $eassoc->{massoc}=$massoc;

        # Choose a reference EAssoc from which we can compute the type,
        # command, etc. This is the EAssoc with the 'highest' install mode
        # i.e. the lowest score
        if (!$massoc->{ref_eassoc} or
            $mode_to_score{$mode} < $mode_to_score{$massoc->{mode}})
        {
            $massoc->{ref_eassoc}=$eassoc;
            $massoc->{mode}=$eassoc->{mode};
        }
    }

    foreach my $massoc (values %$massocs)
    {
        my $ref_eassoc=$massoc->{ref_eassoc};
        $massoc->{type}=$ref_eassoc->{type};
        $massoc->{command}=$ref_eassoc->{command};
        cxlog("  $massoc->{id} | $massoc->{type} | $massoc->{mode}\n");

        foreach my $eassoc (values %{$massoc->{eassocs}})
        {
            # Verify that the options of the current eassoc
            # are compatible with the first one.
            if ($eassoc->{type} ne $massoc->{type})
            {
                cxwarn("ignoring '$eassoc->{id}' because its type is incompatible with '$ref_eassoc->{id}'\n");
                $eassoc->{mode}="ignore";
            }
            if ($eassoc->{command} ne $massoc->{command})
            {
                cxwarn("ignoring '$eassoc->{id}' because its command is incompatible with '$ref_eassoc->{id}'\n");
                $eassoc->{mode}="ignore";
            }
        }
    }
    return ($massocs, $eassocs, $winmimes, $winexts);
}



# Process command-line options
my $opt_create;
my $opt_delete;
my $opt_delete_filter;
my $opt_mode;
my $opt_mode_filter;
my $opt_query;
my $opt_query_filter;
my $opt_list_files;
my $opt_list_files_filter;
my $opt_install;
my $opt_install_filter;
my $opt_uninstall;
my $opt_uninstall_filter;
my $opt_removeall;
my $opt_destdir;
my $opt_sync;
my $opt_sync_mode;
my $opt_sync_install_none;
my $opt_sync_uninstall_none;
my $opt_utf8;
my $opt_bottle;
my $opt_crossover;
my $opt_tie;
my $opt_conf;
my $opt_tag;
my $opt_pattern;
my $opt_filter;
my $opt_ignorelist;
my $opt_ro_desktopdata;
my $opt_mime;
my $opt_description;
my $opt_infotip;
my $opt_appname;
my $opt_verbname;
my $opt_type;
my $opt_icon;
my $opt_command;
my $opt_scope;
my $opt_assoc_scope;
my $opt_verbose;
my $opt_help;
require CXOpts;
my $cxopts=CXOpts->new();
$cxopts->add_options(["create=s"      => \$opt_create,
                      "delete"        => \$opt_delete,
                      "delete-filter=s" => \$opt_delete_filter,
                      "mode=s"        => \$opt_mode,
                      "mode-filter=s" => \$opt_mode_filter,
                      "query"         => \$opt_query,
                      "query-filter=s" => \$opt_query_filter,
                      "list-files"    => \$opt_list_files,
                      "list-files-filter=s" => \$opt_list_files_filter,
                      "install"       => \$opt_install,
                      "install-filter=s" => \$opt_install_filter,
                      "uninstall"     => \$opt_uninstall,
                      "uninstall-filter=s" => \$opt_uninstall_filter,
                      "removeall!"    => \$opt_removeall,
                      "destdir=s"     => \$opt_destdir,
                      "sync"          => \$opt_sync,
                      "sync-mode"     => \$opt_sync_mode,
                      "sync-install-none" => \$opt_sync_install_none,
                      "sync-uninstall-none" => \$opt_sync_uninstall_none,
                      "utf8"          => \$opt_utf8,
                      "bottle=s"      => \$opt_bottle,
                      "crossover"     => \$opt_crossover,
                      "tie"           => \$opt_tie,
                      "conf=s"        => \$opt_conf,
                      "tag=s"         => \$opt_tag,
                      "pattern=s"     => \$opt_pattern,
                      "filter=s"      => \$opt_filter,
                      "ignorelist=s"  => \$opt_ignorelist,
                      "ro-desktopdata"=> \$opt_ro_desktopdata,
                      "mime=s"        => \$opt_mime,
                      "description=s" => \$opt_description,
                      "infotip=s"     => \$opt_infotip,
                      "appname=s"     => \$opt_appname,
                      "verbname=s"    => \$opt_verbname,
                      "type=s"        => \$opt_type,
                      "icon=s"        => \$opt_icon,
                      "command=s"     => \$opt_command,
                      "scope=s"       => \$opt_scope,
                      "assoc-scope=s" => \$opt_assoc_scope,
                      "verbose!"      => \$opt_verbose,
                      "?|h|help"      => \$opt_help
                     ]);
my $err=$cxopts->parse();
CXLog::fdopen(2) if ($opt_verbose);
CXLog::set_default_channel("cxassoc");


# Validate the command line options
my $usage;
my ($read_only, $only_non_empty, $only_removeall);
my ($default_mode, $hash_mode);
if ($err)
{
    cxerr("$err\n");
    $usage=2;
}
elsif ($opt_help)
{
    $usage=0;
}
else
{
    if (!$opt_utf8)
    {
        # Re-encode the parameters to UTF-8
        require CXRecode;
        $opt_create=CXRecode::from_sys("UTF-8", $opt_create);
        # $opt_(delete_|mode_|query_|list_files_|install_|uninstall_)filter:
        # no need to recode the filter options because they are mangled
        $opt_description=CXRecode::from_sys("UTF-8", $opt_description);
        $opt_infotip=CXRecode::from_sys("UTF-8", $opt_infotip);
        $opt_appname=CXRecode::from_sys("UTF-8", $opt_appname);
        $opt_verbname=CXRecode::from_sys("UTF-8", $opt_verbname);
        $opt_command=CXRecode::from_sys("UTF-8", $opt_command);
        $opt_icon=CXRecode::from_sys("UTF-8", $opt_icon);
    }
    if ($opt_crossover and $opt_tie)
    {
        cxerr("--crossover and --tie are mutually exclusive\n");
        $usage=2;
    }
    elsif ($opt_crossover)
    {
        $opt_conf="$ENV{CX_ROOT}/share/crossover/data/crossover.assoc" if (!defined $opt_conf);
        $opt_tag=CXUtils::get_product_id() . "-0" if (!defined $opt_tag);
        $opt_scope="managed";
        delete $ENV{CX_BOTTLE};
    }
    elsif ($opt_tie)
    {
        $opt_conf="$ENV{CX_ROOT}/share/crossover/data/cxtie.assoc" if (!defined $opt_conf);
        $opt_tag=CXUtils::get_product_id() . "-1" if (!defined $opt_tag);
        $opt_scope="managed";
        delete $ENV{CX_BOTTLE};
    }

    my $cmd_count=0;
    $cmd_count++ if (defined $opt_query);
    $cmd_count++ if (defined $opt_list_files);
    my $read_only_count=$cmd_count;
    $cmd_count++ if (defined $opt_removeall);
    $cmd_count++ if (defined $opt_create);
    $cmd_count++ if (defined $opt_mode and !$opt_sync);
    $cmd_count++ if (defined $opt_sync);
    $only_non_empty=1 if ($cmd_count == 0);
    $cmd_count++ if (defined $opt_install);
    $cmd_count++ if (defined $opt_uninstall);
    $cmd_count++ if (defined $opt_delete);
    $read_only=1 if ($read_only_count == $cmd_count);
    $only_removeall=1 if ($cmd_count == 1 and $opt_removeall);
    if ($cmd_count == 0)
    {
        cxerr("you must specify the operation to perform\n");
        $usage=2;
    }
    elsif ($opt_sync)
    {
        if ($opt_sync_uninstall_none and ($opt_uninstall or $opt_removeall))
        {
            cxerr("--sync-uninstall-none is incompatible with --uninstall and --removeall\n");
            $usage=2;
        }
        if ($opt_sync_install_none and $opt_install)
        {
            cxerr("--sync-install-none is incompatible with --install\n");
            $usage=2;
        }
    }
    elsif ($opt_sync_mode or $opt_sync_install_none or $opt_sync_uninstall_none)
    {
        cxerr("--sync-mode, --sync-install-none and --sync-uninstall-none can only be used with --sync\n");
        $usage=2;
    }
    if (($opt_create or $opt_sync) and
        (defined $opt_filter or defined $opt_delete_filter or
         defined $opt_mode_filter or defined $opt_query_filter or
         defined $opt_list_files_filter or
         defined $opt_install_filter or defined $opt_uninstall_filter))
    {
        cxerr("--create and --sync are incompatible with the --filter options\n");
        $usage=2;
    }
    if (defined $opt_mode)
    {
        # Prepare and check the $opt_mode specification
        foreach my $mode_spec (split /;+/, $opt_mode)
        {
            $mode_spec =~ s/^alternate\b/alternative/i;
            if ($mode_spec !~ /^(default|alternative|mime|ignore)(?:=(.*))?$/i)
            {
                cxerr("unknown install mode specification '$mode_spec'\n");
                $usage=2;
                last;
            }
            my ($mode, $regexp_list)=($1, $2);
            if (defined $regexp_list)
            {
                foreach my $regexp (split /:+/, $regexp_list)
                {
                    next if ($regexp eq "");
                    if (!defined $hash_mode->{$mode})
                    {
                        $hash_mode->{$mode}=$regexp;
                    }
                    else
                    {
                        $hash_mode->{$mode}.="|$regexp";
                    }
                }
            }
            elsif (defined $default_mode)
            {
                cxerr("'$mode_spec': the default mode can only be set once\n");
                $usage=2;
                last;
            }
            else
            {
                $default_mode=$mode;
            }
        }
        if (CXLog::is_on())
        {
            while (my ($mode, $regexp)=each %$hash_mode)
            {
                cxlog("Install mode for ($regexp) -> $mode\n");
            }
            if (defined $default_mode)
            {
                cxlog("Default install mode -> $default_mode\n");
            }
        }
    }
    if (defined $opt_create)
    {
        $opt_type="raw" if (!$opt_type);
        $opt_type=~ tr/A-Z/a-z/;
        if ($opt_type !~ /^(?:raw|windows)$/)
        {
            cxerr("unknown association type '$opt_type'\n");
            $usage=2;
        }
    }
    elsif (defined $opt_mime or defined $opt_description or
           defined $opt_infotip or defined $opt_appname or
           defined $opt_verbname or defined $opt_type or defined $opt_icon or
           defined $opt_command)
    {
        cxerr("--mime, --description, --infotip, --appname, --verbname, --type, --icon and --command options can only be used with --create\n");
        $usage=2;
    }
    if (defined $opt_delete_filter and !defined $opt_delete)
    {
        cxerr("--delete-filter can only be used with --delete\n");
        $usage=2;
    }
    if (defined $opt_mode_filter and !defined $opt_mode)
    {
        cxerr("--mode-filter can only be used with --mode\n");
        $usage=2;
    }
    if (defined $opt_query_filter and !defined $opt_query)
    {
        cxerr("--query-filter can only be used with --query\n");
        $usage=2;
    }
    if (defined $opt_list_files_filter and !defined $opt_list_files)
    {
        cxerr("--list-files-filter can only be used with --list-files\n");
        $usage=2;
    }
    if (defined $opt_install_filter and !defined $opt_install)
    {
        cxerr("--install-filter can only be used with --install\n");
        $usage=2;
    }
    if (defined $opt_uninstall_filter and !defined $opt_uninstall)
    {
        cxerr("--uninstall-filter can only be used with --uninstall\n");
        $usage=2;
    }
    if (defined $opt_filter)
    {
        if (!defined $opt_delete and !defined $opt_mode and
            !defined $opt_query and !defined $opt_install and
            !defined $opt_uninstall)
        {
            cxerr("--filter can only be used with --install, --mode, --query, --uninstall and --delete\n");
            $usage=2;
        }
        if (defined $opt_delete_filter or defined $opt_mode_filter or
            defined $opt_query_filter or defined $opt_list_files_filter or
            defined $opt_install_filter or defined $opt_uninstall_filter)
        {
            cxerr("--delete-filter, --mode-filter, --query-filter, --list-files, --install-filter and --uninstall-filter are incompatible with --filter\n");
            $usage=2;
        }
        else
        {
            $opt_delete_filter=$opt_mode_filter=$opt_query_filter=$opt_list_files_filter=$opt_install_filter=$opt_uninstall_filter=$opt_filter;
        }
    }
    if (defined $opt_destdir and !defined $opt_query and
        !defined $opt_list_files and !defined $opt_install and
        !defined $opt_uninstall and !defined $opt_removeall)
    {
        cxerr("--destdir can only be used with --querry, --list-files, --install, --uninstall and --removeall\n");
        $usage=2;
    }
    $opt_destdir ||= "";
    if (defined $opt_ro_desktopdata and !defined $opt_install and
        !defined $opt_uninstall and !defined $opt_removeall)
    {
        cxerr("--ro-desktopdata must be used with one of --install, --uninstall or --removeall\n");
        $usage=2;
    }
    if (defined $opt_tag and $opt_tag eq "")
    {
        cxerr("specifying an empty tag is not allowed\n");
        $usage=2;
    }
    if (defined $opt_pattern)
    {
        if (!$opt_removeall)
        {
            cxerr("--pattern can only be used with --removeall\n");
            $usage=2;
        }
        if (length($opt_pattern) < 4)
        {
            cxerr("the pattern must be at least 4 characters long\n");
            $usage=2;
        }
    }
    if (defined $opt_scope)
    {
        $opt_scope=~ tr/A-Z/a-z/;
        if ($opt_scope !~ /^(?:managed|private)$/)
        {
            cxerr("unknown scope value '$opt_scope'\n");
            $usage=2;
        }
    }
    elsif (defined $opt_conf or ($only_removeall and defined $opt_pattern))
    {
        $opt_scope="private";
    }
}


# Determine and check wineprefix
my $cxconfig;
if (!defined $usage)
{
    require CXBottle;
    $cxconfig=CXBottle::get_crossover_config();

    $ENV{CX_BOTTLE}=$opt_bottle if (defined $opt_bottle);
    $ENV{CX_BOTTLE}="default" if (!defined $ENV{CX_BOTTLE} and !defined $opt_conf and !($only_removeall and defined $opt_pattern));
    if (defined $ENV{CX_BOTTLE})
    {
        # Some desktop plugins need the WINEPREFIX
        my ($scope)=CXBottle::setup_bottle_wineprefix($opt_scope);
        if (!defined $scope)
        {
            cxerr($@);
            $usage=1;
        }
        else
        {
            CXBottle::setup_bottle_environment($cxconfig, $ENV{WINEPREFIX});
            if (!$read_only and
                CXBottle::get_bottle_mode($cxconfig, $opt_scope) eq "stub")
            {
                cxerr("cannot operate on the '$ENV{CX_BOTTLE}' stub bottle (missing --scope option?)\n");
                $usage=1;
            }
            $opt_scope=$scope;
        }
    }
    else
    {
        # Delete WINEPREFIX in case it is set in the user's environment
        delete $ENV{WINEPREFIX};
    }
}


# Print usage
if (defined $usage)
{
    my $name0=cxname0();
    if ($usage)
    {
        cxerr("try '$name0 --help' for more information\n");
        exit $usage;
    }
    print "Usage: $name0 --sync [--bottle BOTTLE] [--mode MODE] [--verbose]\n";
    print "or     $name0 [--bottle BOTTLE] [--crossover] [--install] [--uninstall]\n";
    print "               [--removeall [--pattern PATTERN]] [--mode MODE] [--help]\n";
    print "               [--create ASSOC [create-options]|--filter FILTER] [--delete]\n";
    print "               [advanced-options]\n";

    print "\n";
    print "Makes the Windows associations available to the native desktop environment\n";
    print "and provides a command-line interface for managing these associations.\n";

    print "\n";
    print "Options:\n";
    print "  --sync      Scans the bottle's associations and updates the association\n";
    print "              list accordingly. New associations will receive the mode\n";
    print "              specified by the --mode option and will be installed if\n";
    print "              appropriate. Obsolete associations will be uninstalled and\n";
    print "              deleted\n";
    print "    --sync-mode Override the association install mode even if it exists already\n";
    print "    --sync-uninstall-none Instructs --sync not to uninstall and delete obsolete\n";
    print "                associations\n";
    print "    --sync-install-none Instructs --sync not to (re)install modified or new\n";
    print "                associations\n";
    print "  --bottle BOTTLE Use the specified bottle. If this option is not used,\n";
    print "              fallback to \$CX_BOTTLE and then to 'default'\n";
    print "  --crossover Sets the correct options for working on the CrossOver\n";
    print "              associations\n";
    print "  --mode MODE Sets the association install mode. The supported modes are:\n";
    print "              ignore      - Ignore the association. See --install\n";
    print "              mime        - Export the MIME type but not the association\n";
    print "              alternative - Export the MIME type and the association\n";
    print "              default     - Export the MIME type and make this association\n";
    print "                            the default one for this MIME type\n";
    print "              MODE can also be a semi-colon separated list of strings of the\n";
    print "              form 'MODE=REGEXP1:REGEXP2:...', where MODE is one of the above\n";
    print "              and applies to all associations matching the specified regular\n";
    print "              expressions. See the --create option for more details on how\n";
    print "              to identify an association\n";
    print "    --mode-filter Specifies the associations to set the mode on. See --filter\n";
    print "  --install   Exports the associations so they can be used from the\n";
    print "              supported desktop environments (e.g. GNOME, KDE)\n";
    print "              This operation ignores associations that have their install\n";
    print "              mode set to 'ignore'\n";
    print "    --install-filter Specifies the associations to install. See --filter\n";
    print "  --uninstall Removes the associations from the desktop environments, even if\n";
    print "              their install mode is 'ignore'\n";
    print "    --uninstall-filter Specifies the associations to uninstall. See --filter\n";
    print "  --removeall Each bottle has a unique id which is used to tag its\n";
    print "              associations. This option uninstalls any association bearing\n";
    print "              the bottle's id\n";
    print "    --pattern PATTERN Specifies that --removeall should uninstall any\n";
    print "                association that bears an id matching the specified regular\n";
    print "                expression\n";
    print "  --help, -h  Shows this help message\n";

    print "\n";
    print "Advanced options:\n";
    print "  --create ASSOC Creates the specified association. ASSOC must be a string of\n";
    print "              the form '.EXT/CLASS/VERB', where EXT is the file extension to\n";
    print "              associate to, CLASS is the Windows class, and VERB is the\n";
    print "              Windows association verb. If CLASS and VERB are empty they can\n";
    print "              be omitted\n";
    print "    --mime MIME The association's MIME type\n";
    print "    --description DESCRIPTION The MIME type's description\n";
    print "    --infotip INFOTIP The MIME type's infotip\n";
    print "    --appname APPNAME The application's name\n";
    print "    --verbname VERBNAME The verb's full name\n";
    print "    --type TYPE Either 'windows' for Windows associations, or 'raw' for\n";
    print "                arbitrary associations\n";
    print "    --icon ICONFILE Native path to the XPM icon file. Environment variables\n";
    print "                are allowed using the \${VAR} syntax\n";
    print "    --command COMMAND The command to run for this association. Environment\n";
    print "                variables are allowed using the \${VAR} syntax\n";
    print "  --filter FILTER Specifies the associations to operate on. FILTER is a\n";
    print "              colon-separated list of associations, where each association is\n";
    print "              identified by an ASSOC string in the format described in the\n";
    print "              --create option. By default the operations apply to all known\n";
    print "              associations\n";
    print "  --delete    Deletes the associations. Note that this does not uninstall\n";
    print "              them\n";
    print "    --delete-filter Specifies the associations to delete. See --filter\n";
    print "  --query     Outputs the list of supported association systems for each\n";
    print "              install mod. Then, for each association and each install mode,\n";
    print "              display a list of the corresponding association systems\n";
    print "    --query-filter Specifies the associations to query. See --filter\n";
    print "  --list-files Reports the files used by the association systems.\n";
    print "    --list-files-filter Restricts the association system files reporting to those";
    print "              of the specified associations. See --filter\n";
    print "  --destdir DIR Prefixes the specified directory to all created paths\n";
    print "  --ignorelist LIST Specifies a colon-separated list of association systems\n";
    print "              to ignore. If the list starts with a '+' then it is appended\n";
    print "              to the 'AssocIgnoreList' setting. Otherwise it overrides it\n";

    print "  --utf8      Specifies that the script parameters are in UTF-8, regardless of\n";
    print "              the default system encoding\n";
    print "  --conf FILE Specifies an alternative file containing the association list\n";
    print "  --tag TAG   Specifies an alternative tag to be used for identifying the\n";
    print "              associations\n";
    print "  --ro-desktopdata Treat the desktopdata / bottle directory as read-only\n";
    print "  --scope SCOPE Specifies whether to look for a managed bottle or a private\n";
    print "              one\n";
    print "  --assoc-scope ASCOPE Setting this should almost never be needed as it defaults\n";
    print "              to the scope of the bottle. If set to managed and given\n";
    print "              administrative rights, the associations will be installed in a\n";
    print "              system-wide location. Otherwise they will be installed for use in\n";
    print "              the current account only\n";
    print "  --verbose   Output more information about what is going on\n";
    exit 0;
}


# Because of user installs and builtin bottles the effective scope of the
# associations may be different from the bottle (or other assumed) scope.
if (!defined $opt_assoc_scope)
{
    $opt_assoc_scope="private";
    if ($opt_scope eq "managed")
    {
        my $ref_dir=$ENV{WINEPREFIX} || $ENV{CX_ROOT};
        $opt_assoc_scope="managed" if ((stat($ref_dir))[4] == 0);
    }
}
if ($opt_assoc_scope eq "managed")
{
    # Make sure the associations are going to be world-readable
    umask(umask() & ~0055) if ($> == 0);
}

my $bottle_mode=$cxconfig->get("Bottle", "AssocMode", "");
$bottle_mode =~ tr/A-Z/a-z/;
$bottle_mode="ignore" if ($bottle_mode !~ /^(?:install|ignore|frozen)$/);
if ($bottle_mode eq "frozen" and !$read_only)
{
    cxerr("installing, uninstalling or modifying the bottle's associations is forbidden because they are frozen\n");
    exit 1;
}

my $rc=0;
my $lock_conf_name;
if (defined $ENV{WINEPREFIX})
{
    require CXBottle;
    if (!$only_removeall and
        !CXBottle::update_bottle($cxconfig, $ENV{WINEPREFIX}, $opt_scope))
    {
        cxerr("unable to update the '$ENV{CX_BOTTLE}' bottle\n");
        exit 1;
    }
    if (!defined $opt_conf)
    {
        $lock_conf_name=CXBottle::get_bottle_lock_prefix($ENV{WINEPREFIX}) . "-cxassoc.conf";
        $opt_conf="$ENV{WINEPREFIX}/cxassoc.conf";
    }

    if (($opt_install or $opt_uninstall or $opt_removeall) and
        !$opt_ro_desktopdata and !$opt_create and
        !defined $opt_install_filter and !defined $opt_uninstall_filter)
    {
        my $mode=($opt_install ? "install" : "ignore");
        if ($mode ne $bottle_mode)
        {
            # Serialize cxbottle.conf modifications
            my $lock=CXBottle::lock_bottle($ENV{WINEPREFIX});

            require CXRWConfig;
            my $cxbottle=CXRWConfig->new("$ENV{WINEPREFIX}/cxbottle.conf");
            $cxbottle->set("Bottle", "AssocMode", $mode);
            if (!$cxbottle->save())
            {
                cxerr("unable to save '$ENV{WINEPREFIX}/cxbottle.conf': $!\n");
                $rc=1;
            }

            CXUtils::cxunlock($lock);
        }
    }
}

my ($cxassoc, $lock_conf);
if (defined $opt_conf)
{
    if ($opt_create or $opt_mode or $opt_sync or $opt_delete)
    {
        # Serialize modifications to the cxassoc.conf file
        # To avoid deadlocks only take this lock after upgrading the bottle
        $lock_conf_name=cxbasename($opt_conf) if (!defined $lock_conf_name);
        $lock_conf=CXUtils::cxlock($lock_conf_name);
        if (!defined $lock_conf)
        {
            cxwarn("$@\n");
            cxwarn("locking failed, continuing without a lock\n");
        }
    }
    require CXRWConfig;
    $cxassoc=CXRWConfig->new($opt_conf);
}
if ($only_non_empty and !$cxassoc->get_section_names())
{
    # The file is empty so skip on loading the plugins.
    # There is nothing to do anyway.
    CXUtils::cxunlock($lock_conf);
    exit $rc;
}
if ($cxassoc)
{
    # We cannot trust the cxassoc.conf section names because the raw mangled
    # form is not canonical. For instance '.'=='^2E'=='^2e'.
    # So before we can safely use eassocids in hashtables and comparisons we
    # must normalize them.
    foreach my $eassocid ($cxassoc->get_section_names())
    {
        my $neassocid=join("/", normalize_eassocid($eassocid));
        if ($eassocid ne $neassocid)
        {
            cxlog("renaming [$eassocid] section to [$neassocid]\n");
            my $section=$cxassoc->rename_section($eassocid, $neassocid);

            # Now we may need to set VerbName in order to preserve the
            # eassocid's verb case
            my $verb=(split "/", $eassocid)[2] || "";
            if ($verb =~ /[A-Z]/)
            {
                my $verbname=$section->get("VerbName", "");
                if ($verbname eq "")
                {
                    $verbname=demangle_string($verb);
                    my $nverb=demangle_string((split "/", $neassocid)[2] || "");
                    if ($verbname ne maybe_capitalize($nverb))
                    {
                        $section->set("VerbName", $verbname);
                    }
                }
            }
        }
    }
}

# Finally do the work
if ($opt_create)
{
    $default_mode="default" if (!$default_mode);
    my $section=$cxassoc->append_section($opt_create);
    # Remove all the old fields so the old association is completely
    # overwritten
    $section->remove_all();
    $section->set("MimeType", $opt_mime) if (defined $opt_mime);
    $section->set("Description", $opt_description) if (defined $opt_description);
    $section->set("InfoTip", $opt_infotip) if (defined $opt_infotip);
    $section->set("AppName", $opt_appname) if (defined $opt_appname);
    $section->set("VerbName", $opt_verbname) if (defined $opt_verbname);
    $section->set("Type", $opt_type);
    $section->set("Command", $opt_command) if (defined $opt_command);
    $section->set("Icon", $opt_icon) if (defined $opt_icon);
    $section->set("Mode", get_install_mode($opt_create, $hash_mode, $default_mode));

    # The other operations will work on this one association
    $opt_delete_filter=$opt_mode_filter=$opt_query_filter=$opt_list_files_filter=$opt_install_filter=$opt_uninstall_filter=$opt_create;
}
elsif ($opt_sync)
{
    if (!defined $ENV{WINEPREFIX})
    {
        cxerr("no bottle was specified for the --sync option\n");
        CXUtils::cxunlock($lock_conf);
        exit 1;
    }

    my $icon_dir=CXAssoc::get_icon_dir();
    if (!cxmkpath($icon_dir))
    {
        cxwarn("unable to create the '$icon_dir' directory: $@\n");
    }

    $default_mode="ignore" if (!$default_mode);
    my %to_delete;
    foreach my $eassocid ($cxassoc->get_section_names())
    {
        my $type=$cxassoc->get($eassocid, "Type", "raw");
        $to_delete{$eassocid}=1 if ($type =~ /^windows$/i);
    }

    # Scan the Windows associations, updating cxassoc.conf
    my $start=CXLog::cxtime();
    my $cmd=shquote_string("$ENV{CX_ROOT}/bin/wine") .
            " --scope " . $opt_scope .
            " --no-convert --wl-app assocscan.exe --scan --icon-dir " .
            shquote_string($icon_dir) . " |";
    require CXConfig;
    my $cxscan=CXConfig->new($cmd);
    cxlog("-> rc=$rc  (took ", CXLog::cxtime()-$start, " seconds)\n");
    if ($? != 0)
    {
        cxerr("an error occurred while scanning the Windows associations\n");
        CXUtils::cxunlock($lock_conf);
        exit 1;
    }

    my (%icon_files, %to_uninstall, %to_install);
    my $std_verb_names=CXAssoc::std_verb_names();
    my $extignorelist=$cxconfig->get("CrossOver", "ExtIgnoreList", "");
    $extignorelist =~ s/:+/|/g;
    foreach my $eassocid (sort $cxscan->get_section_names())
    {
        my $scanned=$cxscan->get_section($eassocid);
        my $icon=$scanned->get("Icon");
        if (defined $icon)
        {
            # Keep all the icons reported by assocscan,
            # otherwise it will have to recreate them
            my $basename=$icon;
            $basename =~ s/\.xpm$//;
            $icon_files{$icon}=1;
            $icon_files{"$basename.png"}=1;
        }
        if ($scanned->get("MimeOnly"))
        {
            # There's no association, just a MIME type. Ideally we'd still be
            # able to do something with it but not yet. So just keep the icon
            # so assocscan does not have to recreate it next time and then
            # just ignore this entry.
            cxlog("MIME-only entry '$eassocid' -> ignored\n");
            next;
        }

        # Normalize the eassocid before using it
        my $neassocid=join("/", normalize_eassocid($eassocid));
        cxlog("$neassocid\n");

        if ($extignorelist ne "" and
            $neassocid =~ m%^\.(?:$extignorelist)(?:/.*)?$%)
        {
            cxlog(" -> ignored\n");
            next;
        }
        my $mime=$scanned->get("MimeType", "");
        if ($mime ne "" and !valid_mimetype($mime))
        {
            cxwarn(" -> ignoring invalid MIME type $mime\n");
            $mime="";
        }

        my ($modified, $mode);
        my $section=$cxassoc->get_section($neassocid);
        if ($section)
        {
            delete $to_delete{$neassocid};
            if (update_field($section, "MimeType", $mime))
            {
                # We must uninstall this eassoc before its MIME type gets
                # changed in cxassoc.conf because after that we may not be
                # able to do it properly
                $to_uninstall{$neassocid}=1;
                $modified=1;
            }
            if ($opt_sync_mode)
            {
                # We don't want the default mode to override the mode of
                # existing associations. So use 'ignore' (the lowest priority
                # mode) instead
                $mode=get_install_mode($neassocid, $hash_mode, "ignore");
                if ($mode_to_score{$mode} < $mode_to_score{$section->get("Mode")})
                {
                    $section->set("Mode", $mode);
                    $modified=1;
                }
            }
        }
        else
        {
            $section=$cxassoc->append_section($neassocid);
            $mode=get_install_mode($neassocid, $hash_mode, $default_mode);
            $section->set("Mode", $mode);
            $section->set("MimeType", $mime);
            $modified=1;
        }

        $modified|=update_field($section, "Description", $scanned->get("Description"));
        $modified|=update_field($section, "InfoTip", $scanned->get("InfoTip"));
        $modified|=update_field($section, "AppName", $scanned->get("AppName"));
        my $str=$scanned->get("VerbName", "");
        my $nverb=(split "/", $neassocid)[2] || "";
        if ($str ne "")
        {
            # Don't bother saving standard verb names
            $str="" if ($str eq ($std_verb_names->{$nverb} || ""));
        }
        elsif ($nverb =~ /[a-z]/)
        {
            my $verb=demangle_string((split "/", $eassocid)[2] || "");
            $nverb=demangle_string($nverb);
            $str=$verb if ($verb ne $nverb and $verb ne maybe_capitalize($nverb));
        }
        $modified|=update_field($section, "VerbName", $str);
        $modified|=update_field($section, "Type", "Windows");
        $section->remove("Command");
        $modified|=update_field($section, "Icon", $icon);
        if ($modified and !$opt_install and !$opt_sync_install_none)
        {
            $mode=$section->get("Mode", "ignore") if (!defined $mode);
            $to_install{$neassocid}=1 if ($mode ne "ignore");
        }
    }

    # Delete obsolete associations. Do it now so they don't pollute the new
    # associations we are going to install (if any).
    foreach my $neassocid (keys %to_delete)
    {
        $cxassoc->remove_section($neassocid);
    }
    if ($bottle_mode eq "install")
    {
        if ($opt_uninstall)
        {
            # If told to uninstall the existing associations, then we must do
            # it using the old content of the cxassoc.conf file. That's
            # because associations are not independent from each other:
            # for instance modifying the MIME type of '.dot//' may have an
            # impact on the '.doc//' association. So because we have already
            # modified our in-memory copy of cxassoc.conf (as required to
            # install the new associations), we delegate the uninstall task to
            # a separate 'cxassoc --uninstall' process which will use the,
            # as yet, unmodified cxassoc.conf file.
            $rc=1 if (cxsystem("$ENV{CX_ROOT}/bin/cxassoc", "--uninstall"));
        }
        elsif (!$opt_sync_uninstall_none and (%to_delete or %to_uninstall))
        {
            # Take into account the --no-removeall case
            if ((defined $opt_removeall and !$opt_removeall) or
                $opt_sync_install_none)
            {
                # We're told to not use --removeall, so as above we have to
                # do the uninstall in a separate process.
                map { $to_uninstall{$_}=1; } keys %to_delete;
                if (cxsystem("$ENV{CX_ROOT}/bin/cxassoc", "--uninstall",
                             "--utf8", "--filter", join(":", keys %to_uninstall)))
                {
                    $rc=1;
                }
            }
            else
            {
                # The fastest way to uninstall the obsolete or modified
                # associations is to remove them all as we can do it in this
                # process essentially for free. Then we must reinstall them
                # all which is almost free too.
                $opt_removeall=1;
                $opt_install=1;
            }
        }
        if (!$opt_install and %to_install)
        {
            $opt_install_filter=join(":", keys %to_install);
            $opt_install=1;
        }
    }

    # Delete obsolete icon files
    if (opendir(my $dh, $icon_dir))
    {
        foreach my $dentry (readdir $dh)
        {
            if ($dentry =~ /\.(?:png|xpm)$/ and !$icon_files{$dentry})
            {
                cxlog("Deleting '$dentry'\n");
                if (!unlink("$icon_dir/$dentry"))
                {
                    cxwarn("unable to delete '$dentry': $!\n");
                }
            }
        }
        closedir($dh);
    }
}
elsif ($opt_mode)
{
    foreach my $eassocid (@{get_filter_list($cxassoc, $opt_mode_filter)})
    {
        my $section=$cxassoc->get_section($eassocid);
        if ($section)
        {
            my $mode=get_install_mode($eassocid, $hash_mode, $default_mode);
            cxlog("Set mode of '$eassocid' to $mode\n");
            $section->set("Mode", $mode);
        }
        else
        {
            cxerr("association '$eassocid' not found\n");
            $rc=1;
        }
    }
}
if (!$opt_delete)
{
    # Save and unlock the configuration file early
    if (defined $cxassoc and !$cxassoc->save())
    {
        cxerr("unable to save '$opt_conf': $!\n");
        $rc=1;
    }
    CXUtils::cxunlock($lock_conf);
    $lock_conf=undef;
}


my @plugins;
my ($lock_cxassoc, $gui_info, $eassocs);
if ($opt_query or $opt_list_files or $opt_install or $opt_uninstall or $opt_removeall)
{
    if (!defined $opt_tag and !($only_removeall and defined $opt_pattern))
    {
        if (defined $ENV{WINEPREFIX})
        {
            require CXBottle;
            $opt_tag=CXBottle::get_bottle_tag($cxconfig);
            if (!defined $opt_tag)
            {
                cxerr("unable to determine the tag of the $ENV{CX_BOTTLE} bottle\n");
                CXUtils::cxunlock($lock_conf);
                exit 1;
            }
        }
        else
        {
            my $name0=cxname0();
            cxerr("you must specify the tag identifying the associations\n");
            cxerr("try '$name0 --help' for more information\n");
            CXUtils::cxunlock($lock_conf);
            exit 1;
        }
    }

    # Build a centralised repository of cxassoc options relevant to plugins
    require CXBottle;
    my $cxoptions={
        tag         => $opt_tag,
        destdir     => $opt_destdir,
        desktopdata => CXBottle::get_desktopdata_dir($opt_assoc_scope, $opt_tag),
        ro_desktopdata   => $opt_ro_desktopdata,
    };

    # Setup the list of MIME types to ignore
    my $mimeignorelist=$cxconfig->get("CrossOver", "MIMEIgnoreList", "");
    map { $cxoptions->{mimeignorelist}->{$_}=1 } split /:+/, $mimeignorelist;

    # Setup the list of MIME type aliases
    my $section=$cxconfig->get_section("MIMEAliases");
    if ($section)
    {
        my $mimealiases={};
        while (my ($name, $value)=each %{$section->get_fields()})
        {
            my $group=$mimealiases->{$name} || [];
            foreach my $mimetype ($name, split /;+/, $value)
            {
                next if (!$mimetype);
                if (!valid_mimetype($mimetype))
                {
                    cxwarn("invalid MIME type for '$name' alias list\n");
                }
                elsif (!grep /^\Q$mimetype\E$/, @$group)
                {
                    push @$group, $mimetype;
                    $mimealiases->{$mimetype}=$group;
                }
            }
            # We rely on the alias list being sorted alphabetically
            @$group=sort @$group;
            cxlog("Alias group @$group\n");
        }
        $cxoptions->{mimealiases}=$mimealiases;
    }

    # Build the list of Windows MIME types and associations
    if ($opt_install or $opt_uninstall or $opt_query or $opt_list_files)
    {
        ($cxoptions->{massocs}, $eassocs, $cxoptions->{winmimes}, $cxoptions->{winexts})=build_assoc_db($cxassoc);
    }

    # Grab the desktop configuration information
    my $locate_gui="$ENV{CX_ROOT}/bin/locate_gui.sh";
    if (!-f $locate_gui)
    {
        cxerr("'$locate_gui' does not exist\n");
        CXUtils::cxunlock($lock_conf);
        exit 1;
    }
    $locate_gui=shquote_string($locate_gui);
    foreach my $line (cxbackquote("$locate_gui --scope $opt_assoc_scope --assoc", 1))
    {
        chomp $line;
        $gui_info->{$1}=$2 if ($line =~ /^([^=]+)=(.*)$/);
    }

    # Setup the association-system blacklist
    my %ignorehash;
    if (!defined $opt_ignorelist or $opt_ignorelist =~ s/^[+]//)
    {
        my $ignorelist=$cxconfig->get("CrossOver","AssocIgnoreList");
        if (!defined $ignorelist or $ignorelist =~ s/^[+]//)
        {
             map { $ignorehash{$_}=1; } split /:+/, ($gui_info->{desktop_assoc_ignore_list} || "");
        }
        map { $ignorehash{$_}=1; } split /:+/, ($ignorelist || "");
    }
    map { $ignorehash{$_}=1; } split /:+/, ($opt_ignorelist || "");

    # These cannot be blacklisted
    delete $ignorehash{CXAssocWindows};
    delete $ignorehash{CXAssocCheck};

    # Load the desktop plugins
    foreach my $plugin (@desktop_plugins)
    {
        if ($ignorehash{$plugin})
        {
            cxlog("Ignoring association plugin '$plugin'\n");
            next;
        }
        eval "use $plugin;";
        if ($@)
        {
            cxwarn("unable to load '$plugin', ignoring it\n");
            cxlog("$@\n");
            next;
        }
        foreach my $instance ($plugin->detect($cxoptions, $cxconfig, $gui_info))
        {
            my $id=$instance->id();
            if ($ignorehash{$id})
            {
                cxlog("Ignoring association system '$id'\n");
            }
            else
            {
                cxlog("Registering association system '$id'\n");
                push @plugins, $instance;
            }
        }
    }

    # The plugins need a global view of the system's existing associations to
    # figure out what to do. That's hard to do if another process is actively
    # modifying them, so only run one cxassoc process at a time.
    $lock_cxassoc=CXUtils::cxlock("cxassoc");
}

if ($opt_removeall)
{
    my $start=CXLog::cxtime();
    $opt_pattern="$opt_tag\$" if (!defined $opt_pattern);
    foreach my $plugin (@plugins)
    {
        if (CXLog::is_on())
        {
            cxlog("** Removeall '$opt_pattern' from ", $plugin->id(), "\n");
        }
        my $r=$plugin->removeall($opt_pattern);
        if ($r <= 0)
        {
            $rc=1;
            last if ($r < 0);
        }
    }
    cxlog("-> Removeall took ", CXLog::cxtime()-$start, " seconds\n");
}

if ($opt_uninstall)
{
    my $start=CXLog::cxtime();
    my %done;
    foreach my $eassocid (@{get_filter_list($cxassoc, $opt_uninstall_filter)})
    {
        my $massoc=$eassocs->{$eassocid}->{massoc};
        next if (!$massoc or $done{$massoc->{id}});
        $done{$massoc->{id}}=1;

        foreach my $plugin (@plugins)
        {
            if (CXLog::is_on())
            {
                cxlog("** Uninstalling '$eassocid' from ", $plugin->id(), "\n");
            }
            my $r=$plugin->uninstall($massoc);
            if ($r <= 0)
            {
                $rc=1;
                last if ($r < 0);
            }
        }
    }
    cxlog("-> Uninstall took ", CXLog::cxtime()-$start, " seconds\n");
}

if ($opt_install)
{
    my $start=CXLog::cxtime();
    my %done;
    foreach my $eassocid (@{get_filter_list($cxassoc, $opt_install_filter)})
    {
        my $massoc=$eassocs->{$eassocid}->{massoc};
        next if (!$massoc or $done{$massoc->{id}});
        $done{$massoc->{id}}=1;
        next if ($massoc->{mode} eq "ignore");

        # Perform the preinstall step
        foreach my $plugin (@plugins)
        {
            if (CXLog::is_on())
            {
                cxlog("** Preinstall step for '$eassocid' in ", $plugin->id(), "\n");
            }
            my $r=$plugin->preinstall($massoc);
            if ($r <= 0)
            {
                $rc=1;
                last if ($r < 0);
            }
            cxlog("all_exts=", join(" ", sort keys %{$massoc->{all_exts}}), "\n");
        }

        # Proceed to the installation
        foreach my $plugin (@plugins)
        {
            if (CXLog::is_on())
            {
                cxlog("** Installing '$eassocid' to ", $plugin->id(), "\n");
            }
            my $r=$plugin->install($massoc);
            if ($r <= 0)
            {
                $rc=1;
                last if ($r < 0);
            }
        }
    }
    cxlog("-> Install took ", CXLog::cxtime()-$start, " seconds\n");
}

if ($opt_query)
{
    my $start=CXLog::cxtime();
    my $status_lists;
    foreach my $plugin (@plugins)
    {
        my $r=$plugin->query(undef);
        if ($r eq "-1")
        {
            $rc=1;
            last;
        }
        foreach my $status ("default", "alternative", "mime", "partial")
        {
            if ($r->{$status})
            {
                push @{$status_lists->{$status}}, $r->{$status};
            }
        }
    }
    print "[.all]\n";
    foreach my $status ("default", "alternative", "mime", "partial")
    {
        my $list=$status_lists->{$status} || [];
        print "$status=", join(":", @$list), "\n";
    }

    my %done;
    foreach my $eassocid (@{get_filter_list($cxassoc, $opt_query_filter)})
    {
        my $massoc=$eassocs->{$eassocid}->{massoc};
        $status_lists=$massoc ? $done{$massoc->{id}} : {};
        if (!$status_lists)
        {
            $status_lists={};
            foreach my $plugin (@plugins)
            {
                if (CXLog::is_on())
                {
                    cxlog("** Querying '$eassocid' in ", $plugin->id(), "\n");
                }
                my $r=$plugin->query($massoc);
                cxlog($plugin->id(), " returned ", $r, "\n");
                if ($r eq "-1")
                {
                    $rc=1;
                    last;
                }
                if ($r =~ /^(?:default|alternative|mime|partial)$/)
                {
                    push @{$status_lists->{$r}}, $plugin->id();
                }
            }
            $done{$massoc->{id}}=$status_lists;
        }
        print "[$eassocid]\n";
        foreach my $status ("default", "alternative", "mime", "partial")
        {
            my $list=$status_lists->{$status} || [];
            print "$status=", join(":", @$list), "\n";
        }
    }
    cxlog("-> Query took ", CXLog::cxtime()-$start, " seconds\n");
}

if ($opt_list_files)
{
    my $first=1;
    my $start=CXLog::cxtime();
    foreach my $plugin (@plugins)
    {
        my %plugin_files;
        foreach my $eassocid (@{get_filter_list($cxassoc, $opt_query_filter)})
        {
            my $massoc=$eassocs->{$eassocid}->{massoc};
            my $files=$plugin->get_files($massoc);
            if (!defined $files)
            {
                $rc=1;
                last;
            }
            map { $plugin_files{$_}=1 } @$files;
        }
        if (%plugin_files)
        {
            print "\n" if (!$first);
            print "[", $plugin->id(), "]\n";
            for my $file (sort keys %plugin_files)
            {
                $file =~ s%//+%/%g;
                print "\"", escape_string($file), "\"=\"1\"\n";
            }
            $first=undef;
        }
    }
    cxlog("-> ListFiles took ", CXLog::cxtime()-$start, " seconds\n");
}

if ($opt_delete)
{
    my %use_counts;
    foreach my $eassocid ($cxassoc->get_section_names())
    {
        my $icon=$cxassoc->get($eassocid, "Icon", "");
        next if ($icon eq "" or $icon =~ m%^/%);
        if (!defined $use_counts{$icon})
        {
            $use_counts{$icon}=1;
        }
        else
        {
            $use_counts{$icon}++;
        }
    }
    foreach my $eassocid (@{get_filter_list($cxassoc, $opt_delete_filter)})
    {
        cxlog("Deleting association '$eassocid'\n");
        my $icon=expand_string($cxassoc->get($eassocid, "Icon", ""));
        if ($icon ne "" and $icon !~ m%^/%)
        {
            $use_counts{$icon}--;
            if (!$use_counts{$icon})
            {
                $icon=CXAssoc::get_icon($icon);
                if (-f $icon and !unlink $icon)
                {
                    cxwarn("unable to delete '$icon': $!\n");
                }
                $icon=~ s/\.xpm$/.png/;
                if (-f $icon and !unlink $icon)
                {
                    cxwarn("unable to delete '$icon': $!\n");
                }
            }
        }
        $cxassoc->remove_section($eassocid);
    }
    if (defined $ENV{WINEPREFIX})
    {
        CXUtils::garbage_collect_subdirs($ENV{WINEPREFIX}, "/windata/Associations", 0);
    }

    # Would have been saved much earlier if $opt_delete was not set
    if (defined $cxassoc and !$cxassoc->save())
    {
        cxerr("unable to save '$opt_conf': $!\n");
        $rc=1;
    }
    CXUtils::cxunlock($lock_conf);
}

# Let the desktop plugins finish their work
if (@plugins)
{
    my $start=CXLog::cxtime();
    foreach my $plugin (@plugins)
    {
        if (CXLog::is_on())
        {
            cxlog("** Finalizing ", $plugin->id(), "\n");
        }
        $plugin->finalize();
    }
    cxlog("-> Finalization took ", CXLog::cxtime()-$start, " seconds\n");
}
CXUtils::cxunlock($lock_cxassoc);

exit $rc;