Adding side stream changes to Maemian. Working to integrate full upstream libraries...
[maemian] / nokia-lintian / checks / menu-format
diff --git a/nokia-lintian/checks/menu-format b/nokia-lintian/checks/menu-format
new file mode 100644 (file)
index 0000000..ecd2d7b
--- /dev/null
@@ -0,0 +1,908 @@
+# menu format -- lintian check script -*- perl -*-
+
+# 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 script also checks desktop entries, since they share quite a bit of
+# code.  At some point, it would make sense to try to refactor this so that
+# shared code is in libraries.
+#
+# Further things that the desktop file validation should be checking:
+#
+#  - Encoding of the file should be UTF-8.
+#  - Additional Categories should be associated with Main Categories.
+#  - List entries (MimeType, Categories) should end with a semicolon.
+#  - Check for GNOME/GTK/X11/etc. dependencies and require the relevant
+#    Additional Category to be present.
+#  - Check all the escape characters supported by Exec.
+#  - Review desktop-file-validate to see what else we're missing.
+
+package Lintian::menu_format;
+use strict;
+use Tags;
+use Util;
+use File::Basename;
+
+# This is a list of all tags that should be in every menu item.
+my @req_tags=qw(needs section title command);
+
+# This is a list of all known tags.
+my @known_tags=qw(
+       needs
+       section
+       title
+       sort
+       command
+       longtitle
+       icon
+       icon16x16
+       icon32x32
+       description
+       hotkey
+       hints
+    );
+
+# These 'needs' tags are always valid, no matter the context, and no other
+# values are valid outside the Window Managers context (don't include wm here,
+# in other words).  It's case insensitive, use lower case here.
+my @needs_tag_vals=qw(x11 text vc);
+
+# Authorative source of menu sections:
+# http://www.debian.org/doc/packaging-manuals/menu-policy/ch2#s2.1
+
+# This is a list of all valid section on the root menu.
+my @root_sections = ('Applications', 'Games', 'Help', 'Screen',
+                     'Window Managers', 'FVWM Modules', 'Window Maker');
+
+# This is a list of all valid sections a menu item or submenu can go in.
+my @sections = ('Applications/Accessibility',
+               'Applications/Amateur Radio',
+               'Applications/Data Management',
+               'Applications/Editors',
+               'Applications/Education',
+               'Applications/Emulators',
+               'Applications/File Management',
+               'Applications/Graphics',
+               'Applications/Mobile Devices',
+               'Applications/Network/Communication',
+               'Applications/Network/File Transfer',
+               'Applications/Network/Monitoring',
+               'Applications/Network/Web Browsing',
+               'Applications/Network/Web News',
+               'Applications/Office',
+               'Applications/Programming',
+               'Applications/Project Management',
+               'Applications/Science/Astronomy',
+               'Applications/Science/Biology',
+               'Applications/Science/Chemistry',
+               'Applications/Science/Data Analysis',
+               'Applications/Science/Electronics',
+               'Applications/Science/Engineering',
+               'Applications/Science/Geoscience',
+               'Applications/Science/Mathematics',
+               'Applications/Science/Medicine',
+               'Applications/Science/Physics',
+               'Applications/Science/Social',
+               'Applications/Shells',
+               'Applications/Sound',
+               'Applications/System/Administration',
+               'Applications/System/Hardware',
+               'Applications/System/Language Environment',
+               'Applications/System/Monitoring',
+               'Applications/System/Package Management',
+               'Applications/System/Security',
+               'Applications/Terminal Emulators',
+               'Applications/Text',
+               'Applications/TV and Radio',
+               'Applications/Video',
+               'Applications/Viewers',
+               'Applications/Web Development',
+               'Games/Action',
+               'Games/Adventure',
+               'Games/Blocks',
+               'Games/Board',
+               'Games/Card',
+               'Games/Puzzles',
+               'Games/Simulation',
+               'Games/Strategy',
+               'Games/Tools',
+               'Games/Toys',
+               'Help',
+               'Screen/Saving',
+               'Screen/Locking',
+               'Window Managers',
+               'FVWM Modules',
+               'Window Maker'
+              );
+
+# Authorative source of desktop keys:
+# http://standards.freedesktop.org/desktop-entry-spec/1.0/
+#
+# This is a list of all keys that should be in every desktop entry.
+my @req_desktop_keys = qw(Type Name);
+
+# This is a list of all known keys.
+my %known_desktop_keys = map { $_ => 1 }
+    qw(
+       Type
+       Version
+       Name
+       GenericName
+       NoDisplay
+       Comment
+       Icon
+       Hidden
+       OnlyShowIn
+       NotShowIn
+       TryExec
+       Exec
+       Path
+       Terminal
+       MimeType
+       Categories
+       MimeType
+       Categories
+       StartupNotify
+       StartupWMClass
+       URL
+      );
+
+my %deprecated_desktop_keys = map { $_ => 1 }
+    qw(
+       Encoding
+       MiniIcon
+       TerminalOptions
+       Protocols
+       Extensions
+       BinaryPattern
+       MapNotify
+       SwallowTitle
+       SwallowExec
+       SortOrder
+       FilePattern
+      );
+
+# KDE uses some additional keys that should start with X-KDE but don't for
+# historical reasons.  Actions will in theory be in a later version of the
+# standard (it's not mentioned in the current standard, but is implemented by
+# KDE and widely used).
+my %kde_desktop_keys = map { $_ => 1 }
+    qw(
+       ServiceTypes
+       DocPath
+       Keywords
+       InitialPreference
+       Dev
+       FSType
+       MountPoint
+       ReadOnly
+       UnmountIcon
+       Actions
+      );
+
+# Known types of desktop entries.
+# http://standards.freedesktop.org/desktop-entry-spec/1.0/ar01s05.html
+my %known_desktop_types = map { $_ => 1 }
+    qw(
+       Application
+       Link
+       Directory
+      );
+
+# Authorative source of desktop categories:
+# http://standards.freedesktop.org/menu-spec/1.0/apa.html
+
+# This is a list of all Main Categories for .desktop files.  Application is
+# added as an exception; it's not listed in the standard, but it's widely used
+# and used as an example in the GNOME documentation.  GNUstep is added as an
+# exception since it's used by GNUstep packages.
+my %main_categories = map { $_ => 1 }
+    qw(
+       AudioVideo
+       Audio
+       Video
+       Development
+       Education
+       Game
+       Graphics
+       Network
+       Office
+       Settings
+       System
+       Utility
+       Application
+       GNUstep
+      );
+
+# This is a list of all Additional Categories for .desktop files.  Ideally we
+# should be checking to be sure the associated Main Categories are present,
+# but we don't have support for that yet.
+my %categories = map { $_ => 1 }
+    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
+      );
+
+# This is a list of Reserved Categories for .desktop files.  To use one of
+# these, the desktop entry must also have an OnlyShowIn key limiting the
+# environment to one that supports this category.
+my %reserved_categories = map { $_ => 1 }
+    qw(
+       Screensaver
+       TrayIcon
+       Applet
+       Shell
+      );
+
+# 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_tags_hash = map { $_ => 1 } @known_tags;
+my %needs_tag_vals_hash = map { $_ => 1 } @needs_tag_vals;
+my %root_sections_hash = map { $_ => 1 } @root_sections;
+my %sections_hash = map { $_ => 1 } @sections;
+
+# Holds a hash of all files in the package, used for checking for executables.
+my %file_index;
+
+# -----------------------------------
+
+sub run {
+
+my $pkg = shift;
+my $type = shift;
+
+my @menufiles;
+opendir (MENUDIR, "menu/lib") or fail("cannot read menu/lib file directory.");
+push @menufiles, map { "menu/lib/$_" } readdir(MENUDIR);
+closedir MENUDIR;
+opendir (MENUDIR, "menu/share") or fail("cannot read menu/share file directory.");
+push @menufiles, map { "menu/share/$_" } readdir(MENUDIR);
+closedir MENUDIR;
+
+# Find the desktop files in the package for verification and also build a hash
+# of every file in the package to use to verify that the command referenced by
+# a menu item or desktop entry is there.
+my @desktop_files;
+open(IN, '<', "index") or fail("cannot open index file index: $!");
+while (<IN>) {
+    chomp;
+    my ($perm, $owner, $size, $date, $time, $file) = split(' ', $_, 6);
+    $file =~ s,^\./,/,;
+    $file =~ s/ link to .*//;
+    $file =~ s/ -> .*//;
+    my $operm = perm2oct($perm);
+    $file_index{$file} = 1;
+
+    if ($perm =~ m,^-, && $file =~ m,/usr/share/applications/.*\.desktop$,) {
+        if ($perm =~ m,x,o) {
+            tag "executable-desktop-file", sprintf("$file %04o",$operm);
+        }
+        unless (m,template,) {
+            push (@desktop_files, $file);
+        }
+    }
+}
+close IN;
+
+# Verify all the desktop files.
+for my $desktop_file (@desktop_files) {
+    VerifyDesktopFile ($desktop_file, $desktop_file, $pkg);
+}
+
+# Now all the menu files.
+foreach my $menufile (@menufiles) {
+    next if -x $menufile; # don't try to parse executables
+
+    my $basename = basename $menufile;
+    my $fullname = "/usr/share/menu/$basename";
+    $fullname = "/usr/lib/menu/$basename" if $menufile =~ m,^menu/lib/,o;
+
+    next if $basename eq "README"; # README is a special case
+
+    my $menufile_line ="";
+    open (IN, '<', $menufile) or
+       fail("cannot open menu file $menufile for reading.");
+    # line below is commented out in favour of the while loop
+    # do { $_=<IN>; } while defined && (m/^\s* \#/ || m/^\s*$/);
+    while (<IN>) {
+       if (m/^\s*\#/ || m/^\s*$/) {
+           next;
+       } else {
+           $menufile_line = $_;
+           last;
+       }
+    }
+
+    # Check first line of file to see if it matches the old menu file format.
+    if ($menufile_line =~ m/^(?!\?package\(.*\)).* .* .* .* "?.*"? .*$/o) {
+       tag "old-format-menu-file", $fullname;
+       close IN;
+       next;
+    } elsif ($menufile_line =~ m/^!C\s*menu-2/o) {
+       # we can't parse that yet
+       close IN;
+       next;
+    }
+
+    # Parse entire file as a new format menu file.
+    my $line="";
+    my $lc=0;
+    do {
+       $lc++;
+
+       # Ignore lines that are comments.
+       if ($menufile_line =~ m/^\s*\#/o) {
+           next;
+       }
+       $line .= $menufile_line;
+       # Note that I allow whitespace after the continuation character.
+       # This is caught by VerifyLine().
+       if (! ($menufile_line =~ m/\\\s*?$/)) {
+           VerifyLine($pkg,$type,$menufile,$fullname,$line,$lc);
+           $line="";
+       }
+    } while ($menufile_line = <IN>);
+    VerifyLine($pkg,$type,$menufile,$fullname,$line,$lc);
+
+    close IN;
+}
+
+}
+
+# -----------------------------------
+
+# Pass this a line of a menu file, it sanitizes it and
+# verifies that it is correct.
+sub VerifyLine {
+    my ( $pkg, $type, $menufile, $fullname, $line, $linecount ) = @_;
+
+    my %vals;
+
+    chomp $line;
+
+    # Replace all line continuation characters with whitespace.
+    # (do not remove them completely, because update-menus doesn't)
+    $line =~ s/\\\n/ /mgo;
+
+    # This is in here to fix a common mistake: whitespace after a '\'
+    # character.
+    if ($line =~ s/\\\s+\n/ /mgo) {
+       tag "whitespace-after-continuation-character", "$fullname:$linecount";
+    }
+
+    # Ignore lines that are all whitespace or empty.
+    return if $line =~ m/^\s+$/o or ! $line;
+
+    # Ignore lines that are comments.
+    return if $line =~ m/^\s*\#/o;
+
+    # Start by testing the package check.
+    if (not $line =~ m/^\?package\((.*?)\):/o) {
+       tag "bad-test-in-menu-item", "$fullname:$linecount";
+       return;
+    }
+    my $pkg_test = $1;
+    my %tested_packages = map { $_ => 1 } split( /\s*,\s*/, $pkg_test);
+    my $tested_packages = scalar keys %tested_packages;
+    unless (exists $tested_packages{$pkg}) {
+       tag "pkg-not-in-package-test", "$pkg_test $fullname";
+    }
+    $line =~ s/^\?package\(.*?\)://;
+
+    # Now collect all the tag=value pairs. I've heavily commented
+    # the killer regexp that's responsible.
+    #
+    # The basic idea here is we start at the beginning of the line.
+    # Each loop pulls off one tag=value pair and advances to the next
+    # when we have no more matches, there should be no text left on
+    # the line - if there is, it's a parse error.
+    while ($line =~ m/
+          \s*?                 # allow whitespace between pairs
+          (                    # capture what follows in $1, it's our tag
+           [^\"\s=]            # a non-quote, non-whitespace, character
+           *                   # match as many as we can
+          )
+          =
+          (                    # capture what follows in $2, it's our value
+           (?:
+            \"                 # this is a quoted string
+            (?:
+             \\.               # any quoted character
+             |                 # or
+             [^\"]             # a non-quote character
+            )
+            *                  # repeat as many times as possible
+            \"                 # end of the quoted value string
+           )
+           |                   # the other possibility is a non-quoted string
+           (?:
+            [^\"\s]            # a non-quote, non-whitespace character
+            *                  # match as many times as we can
+           )
+          )
+          /ogcx) {
+       my $tag = $1;
+       my $value = $2;
+
+       if (exists $vals{$tag}) {
+           tag "duplicated-tag-in-menu-item", "$fullname $1:$linecount";
+       }
+
+       # If the value was quoted, remove those quotes.
+       if ($value =~ m/^\"(.*)\"$/) {
+           $value = $1;
+       } else {
+           tag "unquoted-string-in-menu-item", "$fullname $1:$linecount";
+       }
+
+       # If the value has escaped characters, remove the
+       # escapes.
+       $value =~ s/\\(.)/$1/g;
+
+       $vals{$tag} = $value;
+    }
+
+    # This is not really a no-op. Note the use of the /c
+    # switch - this makes perl keep track of the current
+    # search position. Notice, we did it above in the loop,
+    # too. (I have a /g here just so the /c takes affect.)
+    # We use this below when we look at how far along in the
+    # string we matched. So the point of this line is to allow
+    # trailing whitespace on the end of a line.
+    $line =~ m/\s*/ogc;
+
+    # If that loop didn't match up to end of line, we have a
+    # problem..
+    if (pos($line) < length($line)) {
+       tag "unparsable-menu-item", "$fullname:$linecount";
+       # Give up now, before things just blow up in our face.
+       return;
+    }
+
+    # Now validate the data in the menu file.
+
+    # Test for important tags.
+    foreach my $tag (@req_tags) {
+       unless ( exists($vals{$tag}) && defined($vals{$tag}) ) {
+           tag "menu-item-missing-required-tag", "$tag $fullname:$linecount";
+           # Just give up right away, if such an essential tag is missing,
+           # chance is high the rest doesn't make sense either. And now all
+           # following checks can assume those tags to be there
+           return;
+       }
+    }
+
+    # Make sure all tags are known.
+    foreach my $tag (keys %vals) {
+       if (! $known_tags_hash{$tag}) {
+           tag "menu-item-contains-unknown-tag", "$tag $fullname:$linecount";
+       }
+    }
+
+    # Sanitize the section tag
+    my $section = $vals{'section'};
+    $section =~ tr:/:/:s;      # eliminate duplicate slashes.
+    $section =~ s:/$::;                # remove trailing slash.
+
+    # Be sure the command is provided by the package.
+    my ($okay, $command) = VerifyCmd ($fullname, $linecount, $vals{'command'}, $pkg);
+    tag "menu-command-not-in-package", "$fullname:$linecount $command"
+        unless ($okay
+                or not $command
+                or ($tested_packages >= 2)
+                or ($section =~ m:^(WindowManagers/Modules|FVWM Modules|Window Maker):));
+
+    if (exists($vals{'icon'})) {
+       VerifyIcon($menufile, $fullname, $linecount, $vals{'icon'}, 32);
+    }
+    if (exists($vals{'icon32x32'})) {
+       VerifyIcon($menufile, $fullname, $linecount, $vals{'icon32x32'}, 32);
+    }
+    if (exists($vals{'icon16x16'})) {
+       VerifyIcon($menufile, $fullname, $linecount, $vals{'icon16x16'}, 16);
+    }
+
+    # Check the needs tag.
+    my $needs = lc($vals{'needs'}); # needs is case insensitive.
+
+    if ($section =~ m:^(WindowManagers/Modules|FVWM Modules|Window Maker):) {
+       # WM/Modules: needs must not be the regular ones nor wm
+       if ($needs_tag_vals_hash{$needs} or $needs eq "wm") {
+           tag "non-wm-module-in-wm-modules-menu-section", "$needs $fullname:$linecount";
+       }
+    } elsif ($section =~ m:^Window ?Managers:) {
+       # Other WM sections: needs must be wm
+        if ($needs ne 'wm') {
+           tag "non-wm-in-windowmanager-menu-section", "$needs $fullname:$linecount";
+       }
+    } else {
+       # Any other section: just only the general ones
+       if ($needs eq "dwww") {
+           tag "menu-item-needs-dwww", "$fullname:$linecount";
+       } elsif (not $needs_tag_vals_hash{$needs}) {
+           tag "menu-item-needs-tag-has-unknown-value", "$needs $fullname:$linecount";
+       }
+    }
+
+    # Check the section tag
+    # Check for historical changes in the section tree.
+    if ($section =~ m:^Apps/Games:) {
+        tag "menu-item-uses-apps-games-section", "$fullname:$linecount";
+        $section =~ s:^Apps/::;
+    }
+    if ($section =~ m:^Apps/:) {
+        tag "menu-item-uses-apps-section", "$fullname:$linecount";
+        $section =~ s:^Apps/:Applications/:;
+    }
+    if ($section =~ m:^WindowManagers:) {
+        tag "menu-item-uses-windowmanagers-section", "$fullname:$linecount";
+        $section =~ s:^WindowManagers:Window Managers:;
+    }
+
+    # Check for Evil new root sections.
+    my ($rootsection) = $section =~ m:([^/]*):;
+    if (not $root_sections_hash{$rootsection}) {
+        if (not $rootsection =~ m/$pkg/i) {
+            tag "menu-item-creates-new-root-section", "$rootsection $fullname:$linecount";
+        }
+    } else {
+        if (not $sections_hash{$section}) {
+            tag "menu-item-creates-new-section", "$vals{section} $fullname:$linecount";
+        }
+    }
+}
+
+
+sub VerifyIcon {
+    my ($menufile, $fullname, $linecount, $icon, $size) = @_;
+    local *IN;
+
+    if ($icon eq 'none') {
+       tag "menu-item-uses-icon-none", "$fullname:$linecount";
+       return;
+    }
+
+    if (not ($icon =~ m/\.xpm$/i)) {
+       tag "menu-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 "menu-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 "menu-icon-too-big", "$icon: ${width}x${height} > ${size}x${size}";
+    }
+
+    close IN or die;
+    return;
+
+parse_error:
+    close IN or die;
+    tag "menu-icon-cannot-be-parsed", "$icon: looking for $parse";
+    return;
+}
+
+
+# Syntax-checks a .desktop file.
+sub VerifyDesktopFile {
+    my ($desktopfile, $file, $pkg) = @_;
+    my %vals;
+    open (DESKTOP, '<', "unpacked/$file")
+       or fail("cannot open desktop file $file: $!");
+    my ($line, $saw_first, $warned_cr);
+    my @pending;
+    while (defined ($line = <DESKTOP>)) {
+        chomp $line;
+        next if ($line =~ m/^\s*\#/ or $line =~ m/^\s*$/);
+        if ($line =~ s/\r//) {
+            tag 'desktop-entry-file-has-crs', "$file:$." unless $warned_cr;
+            $warned_cr = 1;
+        }
+
+        # Err on the side of caution for now.  If the first non-comment line
+        # is not the required [Desktop Entry] group, ignore this file.  Also
+        # ignore any keys in other groups.
+        last if ($saw_first and $line =~ /^\[(.*)\]\s*$/);
+        unless ($saw_first) {
+            return unless $line =~ /^\[Desktop Entry\]\s*$/;
+            $saw_first = 1;
+        }
+
+       # Tag = Value.  For most errors, just add the error to pending rather
+       # than warning on it immediately since we want to not warn on tag
+       # errors if we didn't know the file type.
+       #
+       # 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, $value) = ($1, $2);
+           my $basetag = $tag;
+           my ($encoding) = ($basetag =~ s/\[([^\]]+)\]$//);
+           if (exists $vals{$tag}) {
+               tag "duplicated-key-in-desktop-entry", "$file:$. $tag";
+           } elsif ($deprecated_desktop_keys{$basetag}) {
+               if ($basetag eq 'Encoding') {
+                   push (@pending, [ "desktop-entry-contains-encoding-key", "$file:$. $tag" ]);
+               } else {
+                   push (@pending, [ "desktop-entry-contains-deprecated-key", "$file:$. $tag" ]);
+               }
+           } elsif (    not $known_desktop_keys{$basetag}
+                    and not $kde_desktop_keys{$basetag}
+                    and not $basetag =~ /^X-/) {
+               push (@pending, [ "desktop-entry-contains-unknown-key", "$file:$. $tag" ]);
+           }
+           $vals{$tag} = $value;
+       }
+    }
+    close DESKTOP;
+
+    # Now validate the data in the desktop file, but only if it's a known type.
+    return unless ($vals{'Type'} and $known_desktop_types{$vals{'Type'}});
+
+    # Now we can issue any pending tags.
+    for my $pending (@pending) {
+       tag @$pending;
+    }
+
+    # Test for important keys.
+    for my $tag (@req_desktop_keys) {
+        unless (defined $vals{$tag}) {
+            tag "desktop-entry-missing-required-key", "$file $tag";
+        }
+    }
+
+    # Only test whether the binary is in the package if the desktop file is
+    # directly under /usr/share/applications.  Too many applications use
+    # desktop files for other purposes with custom paths.
+    #
+    # TODO:  Should check quoting and the check special field
+    # codes in Exec for desktop files.
+    if ($file =~ m,^/usr/share/applications/, and $vals{'Exec'} and $vals{'Exec'} =~ /\S/) {
+        my ($okay, $command) = VerifyCmd ($file, undef, $vals{'Exec'}, $pkg);
+        tag "desktop-command-not-in-package", "$file $command"
+            unless $okay or $command eq 'kcmshell';
+    }
+
+    # Check the Category tag.
+    if (defined $vals{'Categories'}) {
+        my @cats = split (';', $vals{'Categories'});
+        my $saw_main;
+        for my $cat (@cats) {
+            next if $cat =~ /^X-/;
+            if ($reserved_categories{$cat}) {
+                tag "desktop-entry-uses-reserved-category", "$cat $file"
+                    unless $vals{'OnlyShowIn'};
+                $saw_main = 1;
+            } elsif (not $categories{$cat} and not $main_categories{$cat}) {
+                tag "desktop-entry-invalid-category", "$cat $file";
+            } elsif ($main_categories{$cat}) {
+                $saw_main = 1;
+            }
+        }
+        unless ($saw_main) {
+            tag "desktop-entry-lacks-main-category", "$file";
+        }
+    }
+}
+
+# Verify whether a command is shipped as part of the package.  Takes the full
+# path to the file being checked (for error reporting) and the binary.
+# Returns a list whose first member is true if the command is present and
+# false otherwise, and whose second member is the command (minus any leading
+# su-to-root wrapper).  Shared between the desktop and menu code.
+sub VerifyCmd {
+    my ($file, $line, $exec, $pkg) = @_;
+    my $location = ($line ? "$file:$line" : $file);
+
+    # 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] and $com[0] eq "/usr/sbin/su-to-root") {
+        tag 'su-to-root-with-usr-sbin', $location;
+    }
+    if ($com[0] and $com[0] =~ m,^(?:/usr/s?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$/) {
+            if (@com) {
+                $cmd = $com[0];
+            } else {
+                $cmd = $wrapper;
+               undef $wrapper;
+            }
+        }
+        tag 'su-wrapper-without--c', "$location $wrapper" unless $cmd;
+       if ($wrapper && $wrapper !~ /su-to-root/ && $wrapper ne $pkg) {
+           tag 'su-wrapper-not-su-to-root', "$location $wrapper";
+       }
+    } else {
+        $cmd = $com[0];
+    }
+    my $okay = $cmd && ($cmd =~ /^[\'\"]/ || $file_index{$cmd} || grep { $file_index{$_ . $cmd} } @path);
+    return ($okay, $cmd);
+}
+
+1;
+
+# Local Variables:
+# indent-tabs-mode: t
+# cperl-indent-level: 4
+# End:
+# vim: syntax=perl ts=8 sw=4