packages feed

xdg-desktop-entry 0.1.1.6 → 0.1.1.7

raw patch · 4 files changed

+101/−5 lines, 4 filesdep +processPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: process

API changes (from Hackage documentation)

- System.Environment.XDG.DesktopEntry: instance GHC.Read.Read System.Environment.XDG.DesktopEntry.DesktopEntry
- System.Environment.XDG.DesktopEntry: instance GHC.Read.Read System.Environment.XDG.DesktopEntry.DesktopEntryType
- System.Environment.XDG.DesktopEntry: instance GHC.Show.Show System.Environment.XDG.DesktopEntry.DesktopEntry
- System.Environment.XDG.DesktopEntry: instance GHC.Show.Show System.Environment.XDG.DesktopEntry.DesktopEntryType
+ System.Environment.XDG.DesktopEntry: deCommandArgs :: DesktopEntry -> Maybe [String]
+ System.Environment.XDG.DesktopEntry: instance GHC.Internal.Read.Read System.Environment.XDG.DesktopEntry.DesktopEntry
+ System.Environment.XDG.DesktopEntry: instance GHC.Internal.Read.Read System.Environment.XDG.DesktopEntry.DesktopEntryType
+ System.Environment.XDG.DesktopEntry: instance GHC.Internal.Show.Show System.Environment.XDG.DesktopEntry.DesktopEntry
+ System.Environment.XDG.DesktopEntry: instance GHC.Internal.Show.Show System.Environment.XDG.DesktopEntry.DesktopEntryType

Files

CHANGELOG.md view
@@ -1,5 +1,12 @@ # Revision history for xdg-desktop-entry +## 0.1.1.7 -- 2026-09-30++* Parse quoted and escaped `Exec` arguments before expanding desktop-entry+  field codes, preserving argument boundaries and literal percent signs.+* Quote expanded arguments safely for shell consumers, including embedded+  apostrophes, dollar signs, and command substitutions (#695).+ ## 0.1.1.6 -- 2026-09-04  * Replace the `ini` based parser with one that follows the desktop entry
src/System/Environment/XDG/DesktopEntry.hs view
@@ -17,6 +17,7 @@ module System.Environment.XDG.DesktopEntry   ( DesktopEntry (..),     deCommand,+    deCommandArgs,     deComment,     deHasCategory,     deIcon,@@ -138,11 +139,70 @@   Maybe String deComment langs de = deLocalisedAtt langs de "Comment" --- | Return the command that should be executed when running this desktop entry.+-- | Return a shell-quoted command for launching this entry without files. deCommand :: DesktopEntry -> Maybe String-deCommand de =-  reverse . dropWhile (== ' ') . reverse . takeWhile (/= '%')-    <$> lookup "Exec" (deAttributes de)+deCommand = fmap (unwords . map shellQuote) . deCommandArgs++-- | Parse and expand Exec arguments without invoking a shell. File and URL+-- placeholders are omitted because no files are being opened.+deCommandArgs :: DesktopEntry -> Maybe [String]+deCommandArgs de = do+  command <- lookup "Exec" (deAttributes de)+  args <- splitExecArgs $ unescapeExecValue command+  expanded <- concat <$> traverse expandArg args+  case expanded of+    program : _ | not (null program) && '=' `notElem` program -> Just expanded+    _ -> Nothing+  where+    expandArg "%i" = pure $ maybe [] (\icon -> if null icon then [] else ["--icon", icon]) (deIcon de)+    expandArg "%F" = Just []+    expandArg "%U" = Just []+    expandArg arg = do+      result <- expandCodes arg+      pure [result | not (null result) || null arg]++    expandCodes [] = Just []+    expandCodes ('%' : '%' : rest) = ('%' :) <$> expandCodes rest+    expandCodes ('%' : code : rest)+      | code `elem` "fudDnNvm" = expandCodes rest+      | code == 'c' = (deName [] de ++) <$> expandCodes rest+      | code == 'k' = (deFilename de ++) <$> expandCodes rest+      | otherwise = Nothing+    expandCodes ['%'] = Nothing+    expandCodes (c : rest) = (c :) <$> expandCodes rest++unescapeExecValue :: String -> String+unescapeExecValue ('\\' : c : rest) =+  case lookup c [('s', ' '), ('n', '\n'), ('t', '\t'), ('r', '\r'), ('\\', '\\')] of+    Just decoded -> decoded : unescapeExecValue rest+    Nothing -> '\\' : c : unescapeExecValue rest+unescapeExecValue (c : rest) = c : unescapeExecValue rest+unescapeExecValue [] = []++splitExecArgs :: String -> Maybe [String]+splitExecArgs = go False False [] []+  where+    go quoted started current args []+      | quoted = Nothing+      | otherwise = Just $ reverse $ if started then reverse current : args else args+    go quoted _ current args ('"' : rest) = go (not quoted) True current args rest+    go _ _ _ _ ['\\'] = Nothing+    go quoted _ current args ('\\' : c : rest)+      | quoted && c `notElem` "\"`$\\" = go quoted True (c : '\\' : current) args rest+      | otherwise = go quoted True (c : current) args rest+    go quoted started current args (c : rest)+      | isSpace c && not quoted =+          go False False [] (if started then reverse current : args else args) rest+      | otherwise = go quoted True (c : current) args rest++shellQuote :: String -> String+shellQuote arg+  | not (null arg) && all safeChar arg = arg+  | otherwise = "'" ++ concatMap escape arg ++ "'"+  where+    safeChar c = isAscii c && (isAlphaNum c || c `elem` "_./:@%+=,-")+    escape '\'' = "'\\''"+    escape c = [c]  -- | Return a list of all desktop entries in the given directory. listDesktopEntries ::
test/Main.hs view
@@ -3,6 +3,7 @@ import System.Environment.XDG.DesktopEntry import System.FilePath ((</>)) import System.IO.Temp (withSystemTempDirectory)+import System.Process (readProcess) import Test.Hspec  fileContent :: [String]@@ -43,6 +44,33 @@     print i     writeFile (filepath i) content   hspec $ do+    describe "Exec arguments" $ do+      let entry command =+            DesktopEntry+              (read "Application")+              "/tmp/Review App.desktop"+              [("Exec", command), ("Name", "Review App"), ("Icon", "review-icon")]+      it "preserves arguments following file placeholders" $+        deCommandArgs (entry "viewer %U --incognito") `shouldBe` Just ["viewer", "--incognito"]+      it "preserves escaped percentages and later flags" $+        deCommandArgs (entry "viewer --title=100%% --new-window")+          `shouldBe` Just ["viewer", "--title=100%", "--new-window"]+      it "expands metadata without splitting arguments" $+        deCommandArgs (entry "viewer %c %k %i")+          `shouldBe` Just ["viewer", "Review App", "/tmp/Review App.desktop", "--icon", "review-icon"]+      it "removes absent icons and deprecated placeholders" $+        deCommandArgs (DesktopEntry (read "Application") "app.desktop" [("Exec", "viewer %i %d --flag")])+          `shouldBe` Just ["viewer", "--flag"]+      it "preserves quoted and empty arguments" $+        deCommandArgs (entry "viewer \"two words\" \"\"") `shouldBe` Just ["viewer", "two words", ""]+      it "rejects unknown field codes, unterminated quotes, and empty commands" $+        map (deCommandArgs . entry) ["viewer %z", "viewer \"unterminated", "", "%U"]+          `shouldBe` replicate 4 Nothing+      it "does not treat desktop arguments as shell expressions" $ do+        let command = deCommand $ entry "printf %%s \"$(printf injected);'\""+        case command of+          Nothing -> expectationFailure "Expected a valid command"+          Just value -> readProcess "sh" ["-c", value] "" `shouldReturn` "$(printf injected);'"     describe "deAtt" $ do       it "content0 should work" $ do         deResultE <- readDesktopEntry $ filepath 0
xdg-desktop-entry.cabal view
@@ -1,6 +1,6 @@ cabal-version:       2.4 name:                xdg-desktop-entry-version:             0.1.1.6+version:             0.1.1.7 synopsis:            Parse files conforming to the xdg desktop entry spec description:         Parse files conforming to the xdg desktop entry spec. bug-reports:         https://github.com/taffybar/taffybar/issues@@ -43,6 +43,7 @@                     base                   , filepath                   , hspec+                  , process                   , temporary                   , unix                   , xdg-desktop-entry