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 +7/−0
- src/System/Environment/XDG/DesktopEntry.hs +64/−4
- test/Main.hs +28/−0
- xdg-desktop-entry.cabal +2/−1
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