[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Bug#433411: lintian: New checks for FreeDesktop's desktop files



Package: lintian 
Version: 1.23.32
Priority: wishlist
Tags: patch

Following the recent discussion at debian-devel related to .desktop files
I've noticed that we lack some lintian checks for common issues in desktop
files (mispelled categories, wrong key value pairs, etc...)

Even though we don't have a policy for Desktop files, since most Desktop
environments (KDE, GNOME) are using them it might be sane to have a check for
desktop files just like we have for menu files.

I've written a first attempt at such check, heavily based on the current
menu-format method and reusing some of its code. It is capable of detecting
wrong Categories and invalid Type values and should be useful to detect
.desktop files which need to be fixed. 

Notice that it could be improved to do much more tests, I'm not sure I'll
handle them myself but maybe this will help others scratch their itch..

Please consider its inclusion into the lintian lab.

Thanks,

Javier
# desktop format -- lintian check script -*- perl -*-

# Copyright (C) 2007 by Javier Fernandez-Sanguino
# based on the menu lintian check written by 
# Copyright (C) 1998 by Joey Hess
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, you can find it on the World Wide
# Web at http://www.gnu.org/copyleft/gpl.html, or write to the Free
# Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
# MA 02110-1301, USA.
# 
# 
# This lintian check aims at implement some standard check for Desktop files
# which are currently not covered by Debian Policy (only menu entries are)
#
# This is a barebones module, it should be extended to check for some more things
# from FreeDesktop's standard:
#
# - Look for Deprecated Items
# - Check field codes in the Exec key
# - Check fields with locales
# - Check Group headers
# - ....
#

package Lintian::desktop_format;
use strict;
use Tags;
use Util;
use File::Basename;

# Authorative source of desktop keys:
# http://standards.freedesktop.org/desktop-entry-spec/desktop-entry-spec-1.0.html
#
# This is a list of all keys that should be in every desktop entry.
my @req_keys=qw(Type Name);

# This is a list of all known keys.
my @known_keys=qw(
        Type
        Name
        Version
        GenericName
        NoDisplay
        Comment
        Icon
        Hidden
        OnlyShowIn
        NotShowIn
        TryExec
        Exec
        Path
        Terminal
        MimeType
        Categories
        StartupNotify
        StartupWMClass
        URL
);
my @deprecated_keys=qw(
        Encoding
        MiniIcon
        TerminalOptions
        Protocols
        Extensions
        BinaryPattern
        MapNotify
        SwallowTitle
        SwallowExec
        SortOrder
        FilePattern
);

# Authorative source of desktop categories
# http://standards.freedesktop.org/menu-spec/menu-spec-1.0.html#category-registry

# This is a list of all valid section on the root menu.
my @root_categories=qw(AudioVideo Audio Video Development Education Game Graphics Network Office Settings System Utility);

# This is a list of all additional categories
my @categories=qw(
        Building
        Debugger
        IDE
        GUIDesigner
        Profiling
        RevisionControl
        Translation
        Calendar
        ContactManagement
        Database
        Dictionary
        Chart
        Email
        Finance
        FlowChart
        PDA
        ProjectManagement
        Presentation
        Spreadsheet
        WordProcessor
        2DGraphics
        VectorGraphics
        RasterGraphics
        3DGraphics
        Scanning
        OCR
        Photography
        Publishing
        Viewer
        TextTools
        DesktopSettings
        HardwareSettings
        Printing
        PackageManager
        Dialup
        InstantMessaging
        Chat
        IRCClient
        FileTransfer
        HamRadio
        News
        P2P
        RemoteAccess
        Telephony
        TelephonyTools
        VideoConference
        WebBrowser
        WebDevelopment
        Midi
        Mixer
        Sequencer
        Tuner
        TV
        AudioVideoEditing
        Player
        Recorder
        DiscBurning
        ActionGame
        AdventureGame
        ArcadeGame
        BoardGame
        BlocksGame
        CardGame
        KidsGame
        LogicGame
        RolePlaying
        Simulation
        SportsGame
        StrategyGame
        Art
        Construction
        Music
        Languages
        Science
        ArtificialIntelligence
        Astronomy
        Biology
        Chemistry
        ComputerScience
        DataVisualization
        Economy
        Electricity
        Geography
        Geology
        Geoscience
        History
        ImageProcessing
        Literature
        Math
        NumericalAnalysis
        MedicalSoftware
        Physics
        Robotics
        Sports
        ParallelComputing
        Amusement
        Archiving
        Compression
        Electronics
        Emulator
        Engineering
        FileTools
        FileManager
        TerminalEmulator
        Filesystem
        Monitor
        Security
        Accessibility
        Calculator
        Clock
        TextEditor
        Documentation
        Core
        KDE
        GNOME
        GTK
        Qt
        Motif
        Java
        ConsoleOnly
);

# Path in which to search for binaries referenced in menu entries.
my @path = qw(/usr/local/bin/ /usr/bin/ /bin/ /usr/X11R6/bin/ /usr/games/);

my %known_keys_hash;
my %deprecated_keys_hash;
my %root_categories_hash;
my %categories_hash;

my %all_files = ();
my %all_links = ();

# -----------------------------------

sub run {

    my $pkg = shift;
    my $type = shift;

# Things worth hashing.
    foreach my $tag (@known_keys) {
        $known_keys_hash{$tag}=1;
    }
    foreach my $tag (@deprecated_keys) {
        $deprecated_keys_hash{$tag}=1;
    }
    foreach my $section (@root_categories) {
        $root_categories_hash{$section}=1;
    }
    foreach my $section (@categories) {
        $categories_hash{$section}=1;
    }

# read package contents
    open(IN,"index") or fail("cannot open index file index: $!");
    while (<IN>) {
        chomp;
        my ($perm,$owner,$size,$date,$time,$file) = split(' ', $_, 6);
        $file =~ s,^(\./),,;
        add_file_link_info ($file);
        $file =~ s/ link to .*//;
        $file =~ s/ -> .*//;
        my $operm = perm2oct($perm);

        if ($perm =~ m,^-,o) { # file checks
# desktop file?
            if ($file =~ m/\.desktop$/o) { # correct permissions?
                if ($perm =~ m,x,o) { 
                    tag "executable-desktop-file", sprintf("$file %04o",$operm);
                }
                # TODO: This should provide the 
                VerifyFile($file, $file);
            }
        }
    }
    close IN;

}

# -----------------------------------

# Pass this a line of a menu file, it sanitizes it and
# verifies that it is correct.
sub VerifyFile {
    my ( $desktopfile, $fullname ) = @_;

    my %vals;

    open (DESKTOP, "unpacked/$fullname") or fail("cannot open desktop file $fullname: $!");
    my $linecount = 0;
    while ( my $line = <DESKTOP> ) {
        chomp $line; 
        $linecount++;
        next if ( $line =~ m/^\s*\#/ or $line = ~ m/^\s*$/);

# Tag = Value
# TODO: We do not check for properly formatted localised values for keys
# but might be worth checking if they are properly formatted (not their value)
        if ( $line =~ /^(.*?)\s*=\s*(.*?)$/ ) {
            my $tag = $1;
            my $value = $2;

            if (exists $vals{$tag}) {
                tag "duplicated-key-in-desktop-entry", "$fullname $1:$linecount";
            }

            $vals{$tag} = $value;
        }
    }
    close DESKTOP;

# Now validate the data in the desktop file.

# Test for important keys.
    foreach my $tag (@req_keys) {
        unless ( exists($vals{$tag}) && defined($vals{$tag}) ) {
            tag "desktop-entry-missing-required-key", "$tag $fullname:$linecount";
        }
    }
# Make sure all keys are known and no deprecated keys are being used
    foreach my $tag (keys %vals) {
        if ( $deprecated_keys_hash{$tag} ) {
            tag "desktop-entry-contains-deprecated-key", "$tag $fullname:$linecount";
        } elsif (! $known_keys_hash{$tag} && $tag !~ /^X-/ ) {
        # TODO: Will produce false positives for keys reserved by KDE
        # which do not use X-KDE (see Standard)
            tag "desktop-entry-contains-unknown-key", "$tag $fullname:$linecount";
        }
    }


# TODO: Check for deprecated keys: Encoding, MiniIcon, TerminalOptions, Protocols, 
#       Extensions, BinaryPattern, MapNotify, SwallowTitle, SwallowExec, SortOrder
#       FilePattern,

    if (exists($vals{'Exec'})) {
	VerifyExec( $fullname, $linecount, $vals{'Exec'});
    }

    if (exists($vals{'Icon'})) {
	VerifyIcon( $fullname, $linecount, $vals{'Icon'}, 32);
    }

# Check the category tag
    if (exists($vals{'Categories'})) {
        # Is the main category there?
        my $ccats = 0; 
        foreach my $cat (keys %root_categories_hash) {
            $ccats++ if  $vals{'Categories'} =~ /$cat;?/;
        }
        if ( $ccats == 0 )  {
		tag "desktop-entry-lacks-main-category", "$fullname:$linecount"; 
        } elsif ( $ccats > 1 )  {
		tag "desktop-entry-multiple-main-category", "$fullname:$linecount";
        }

        foreach my $cat (split(';',$vals{'Categories'})) {
	    if (not $categories_hash{$cat})  {
		tag "desktop-entry-invalid-category", "$cat $fullname:$linecount";
            }
        }
    }

# TODO:
# - Check if the values correspond to the allowed type for a given key (boolean,
#   integer...)

}

sub VerifyCmd {
# Verify if commands are installed with the package.
# TODO: Should check quoting and the check special field codes in Exec

    my ( $fullname, $linecount, $exec) = @_;

    # Read the file index:
    my %file_index;
    open(FILE_INDEX,"index") or fail("cannot open index file index: $!");
    while(<FILE_INDEX>) {
        $file_index{(split /\s+/, $_)[5]} = 1;
    }
    close FILE_INDEX;

# This routine handles su wrappers.  The option parsing here is ugly and dead-simple,
# but it's hopefully good enough for what will show up in desktop files.
# su-to-root and sux require -c options, kdesu optionally allows one, and
# gksu has the command at the end of its arguments.
    my @com = split(' ',$exec);
    my $cmd;
    if ($com[0] eq "/usr/sbin/su-to-root") {
	tag "su-to-root-with-usr-sbin", "$fullname:$linecount";
    } elsif ($com[0] =~ m,^(?:/usr/bin/)?(su-to-root|gksu|kdesu|sux)$,) {
	my $wrapper = $1;
	shift @com;
	while (@com) {
	    unless ($com[0]) {
		shift @com;
		next;
	    }
	    if ($com[0] eq '-c') {
		$cmd = $com[1];
		last;
	    } elsif ($com[0] =~ /^-[Dfmupi]|^--(user|description|message)/) {
		shift @com;
		shift @com;
	    } elsif ($com[0] =~ /^-/) {
		shift @com;
	    } else {
		last;
	    }
	}
	if (!$cmd && $wrapper =~ /^(gk|kde)su$/) {
	    $cmd = $com[0];
	} elsif (!$cmd) {
	    tag "su-wrapper-without--c", "$fullname:$linecount $wrapper";
	}
    } else {
	$cmd = $com[0];
    }
    tag "desktop-command-not-in-package", "$fullname:$linecount $cmd"
	if ($cmd
	    && !($file_index{".$cmd"} || grep {$file_index{".".$_.$cmd}} @path));
}

# TODO: Shouldn't this allow PNG files too?
sub VerifyIcon {
    my ( $fullname, $linecount, $icon, $size) = @_;
    local *IN;

    if ($icon eq 'none') {
	tag "desktop-entry-uses-icon-none", "$fullname:$linecount";
	return;
    }

    if (not ($icon =~ m/\.xpm$/i)) {
	tag "desktop-icon-not-in-xpm-format", "$icon";
	return;
    }

    # Try the explicit location, and if that fails, try the standard path.
    my $iconfile = "unpacked/$icon";
    if (! -f $iconfile) {
	$iconfile = "unpacked/usr/share/pixmaps/$icon";
    }

    if (! open IN, $iconfile) {
	tag "desktop-icon-missing", "$icon";
	return;
    }

    my $parse = "XPM header";
    my $line;
    do { defined ($line = <IN>) or goto parse_error; }
    until ($line =~ /\/\*\s*XPM\s*\*\//);

    $parse = "size line";
    do { defined ($line = <IN>) or goto parse_error; }
    until ($line =~ /"\s*([0-9]+)\s*([0-9]+)\s*([0-9]+)\s*([0-9]+)\s*"/);
    my $width = $1 + 0;
    my $height = $2 + 0;
    my $numcolours = $3 + 0;
    my $cpp = $4 + 0;
    
    if ($width > $size || $height > $size) {
	tag "desktop-icon-too-big", "$icon: ${width}x${height} > ${size}x${size}";
    }

    close IN or die;
    return;

parse_error:
    close IN or die;
    tag "desktop-icon-cannot-be-parsed", "$icon: looking for $parse";
    return;
}

1;

# -----------------------------------

# Add file and link to %all_files and %all_links.  Note that both files and
# links have to include a leading /.
sub add_file_link_info {
    my $file = shift;
    my $link = undef;

    $file = "/" . $file if (not $file =~ m%^/%); # make file absolute
    $file =~ s%/+%/%g;				 # remove duplicated `/'
    ($file, $link) = split(/ -> /, $file);

    $all_files{$file} = 1;

    if (defined $link) {
	if (not $link =~ m,^/,) {		  # not absolute link
	    $link = "/" . $link;		  # make sure link starts with '/'
	    $link =~ s,/+\./+,/,g;		  # remove all /./ parts
	    my $dcount = 1;
	    while ($link =~ s,^/+\.\./+,/,) {	  #\ count & remove
	       $dcount++;			  #/ any leading /../ parts
	    }
	    my $f = $file;
	    while ($dcount--) {			  #\ remove last $dcount
		$f =~ s,/[^/]*$,,;		  #/ path components from $file
	    }
	    $link = $f . $link;			  # now we should have absolute link
	}
	$all_links{$file} = $link unless ($link eq $file);
    }
}



1;

# vim: syntax=perl ts=8 sw=4
Check-Script: desktop-format
Author: Javier Fernandez-Sanguino Pen~a <jfs@debian.rog>
Abbrev: dkf
Standards-Version: 3.6.1
Type: binary
Unpack-Level: 1
Info: This script validates the format of desktop files.

Tag: unparsable-desktop-entry
Type: error
Info: An entry of the desktop file cannot be parsed as a series of key=value
 pairs. 

Tag: duplicated-key-in-desktop-entry
Type: warning
Info: The desktop entry contains two instances of the same key. This is just a
 waste of space, as only one will be used.

Tag: desktop-entry-missing-required-key
Type: error
Info: The desktop entry has a line that is missing a required key.
 See: http://standards.freedesktop.org/desktop-entry-spec/desktop-entry-spec-1.0.html#recognized-keys

Tag: desktop-entry-contains-unknown-key
Type: warning
Info: The desktop entry has a line that has a key in it that is not one
 of the standard keys defined in the FreeDesktop specification and this
 key does not conform to the preferred method for introducing changes
 (via the 'X-' prefix). It's more likely the key's name is misspelled.
 See: http://standards.freedesktop.org/desktop-entry-spec/desktop-entry-spec-1.0.html#recognized-keys

Tag: desktop-entry-contains-deprecated-key
Type: warning
Info: The desktop entry has a line that has a key in it that has been
 deprecated in the FreeDesktop specification.
 See: http://standards.freedesktop.org/desktop-entry-spec/desktop-entry-spec-1.0.html#deprecated-items

Tag: desktop-icon-not-in-xpm-format
Type: error
Info: Icons in the Debian desktop system should be in XPM format.
Ref: desktop manual 3.7

Tag: desktop-icon-missing
Type: warning
Info: This icon file couldn't be found.  If the path to the icon in the
 desktop file is an absolute path, make sure that icon exists at that path in
 the package.  If the path is relative or a simple filename, make sure the
 icon is installed in <tt>/usr/share/pixmaps</tt>, the default location.
 .
 If the icon is in a package this package depends on, add a lintian
 override for this warning.  lintian cannot check icons in other packages.
Ref: desktop manual 3.7

Tag: desktop-icon-too-big
Type: error
Info: Icons in the Debian desktop system should be at most 32x32 pixels
 (icon16x16 icons should of course be at most 16x16 pixels)
Ref: desktop manual 3.7

Tag: desktop-icon-cannot-be-parsed
Type: warning
Info: The icon file could not be parsed.  Perhaps this means a bad XPM file,
 or perhaps it means the lintian parsing needs to be improved.  If the
 window managers and standard tools accept the file then probably it's the
 latter; please file a bug on lintian then.

Tag: desktop-command-not-in-package
Type: warning
Info: The desktop entry specifies a command which is not available in the package.
 In most cases this is a typo or after you moved a binary around, but forgot
 to update the desktop file.

Reply to: