diff --git a/Changelog.md b/Changelog.md index f6a3b8afd..13ff632c6 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,9 @@ # Spectrometer Changelog +## v2.15.6 + +- CocoaPods: Fixes `Podfile.lock` parsing. It safely parses when Pod and Dependencies entries are enclosed with quotations. ([#337](https://github.com/fossas/spectrometer/pull/337)) + ## v2.15.5 - Fixes an issue where `--json` would output the raw project ID, instead of a normalized ID ([#339](https://github.com/fossas/spectrometer/pull/339)) diff --git a/docs/strategies/ios/cocoapods.md b/docs/strategies/ios/cocoapods.md index d486cb9e4..9278a5d53 100644 --- a/docs/strategies/ios/cocoapods.md +++ b/docs/strategies/ios/cocoapods.md @@ -20,8 +20,8 @@ We scan the `Podfile.lock` for two particular sections: `PODS` and dependencies, and `DEPENDENCIES` tells us which of the dependencies the project depends on directly. -In the following example, we have four dependencies, `one`, `two`, `three`, and -`four`. `one`, and `three` are direct dependencies, `one` depends on both +In the following example, we have five dependencies, `one`, `two`, `three`, +`four` and `five/+zlib`. `one`, `three` and `five/+zlib` are direct dependencies, `one` depends on both `two` and `three`, and `three` depends on `four`. ``` @@ -33,8 +33,10 @@ PODS: - three (3.0.0) - four (= 2.3.3) - four (4.0.0): + - "five/+zlib (7.0.0)" DEPENDENCIES: - one (> 4.4) - three (from `Submodules/subproject/.podspec`) + - "five/+zlib (7.0.0)" ``` \ No newline at end of file diff --git a/src/Strategy/Cocoapods/PodfileLock.hs b/src/Strategy/Cocoapods/PodfileLock.hs index 7cdd6c9ea..d1b4fcdfa 100644 --- a/src/Strategy/Cocoapods/PodfileLock.hs +++ b/src/Strategy/Cocoapods/PodfileLock.hs @@ -5,6 +5,10 @@ module Strategy.Cocoapods.PodfileLock ( Dep (..), Pod (..), Section (..), + + -- * for testing, + podParser, + depParser, ) where import Control.Effect.Diagnostics @@ -158,21 +162,22 @@ remoteParser = indentBlock $ do podParser :: Parser Pod podParser = indentBlock $ do - _ <- chunk "- " - name <- findDep + name <- symbol "-" *> ignoreDoubleQuote *> findDep version <- findVersion _ <- restOfLine pure (L.IndentMany Nothing (pure . Pod name version) depParser) depParser :: Parser Dep depParser = do - _ <- chunk "- " - name <- findDep + name <- symbol "-" *> ignoreDoubleQuote *> findDep _ <- restOfLine pure $ Dep name findDep :: Parser Text -findDep = lexeme (takeWhile1P (Just "dep") (not . C.isSpace)) +findDep = lexeme (takeWhile1P (Just "dep") isNotDoubleQuoteAndSpace) + where + isNotDoubleQuoteAndSpace :: Char -> Bool + isNotDoubleQuoteAndSpace c = c /= '"' && (not . C.isSpace) c findVersion :: Parser Text findVersion = between (char '(') (char ')') (lexeme (takeWhileP (Just "version") (/= ')'))) @@ -199,3 +204,9 @@ lexeme = L.lexeme sc sc :: Parser () sc = L.space (void $ some (char ' ')) empty empty + +symbol :: Text -> Parser Text +symbol = L.symbol scn + +ignoreDoubleQuote :: Parser (Maybe Text) +ignoreDoubleQuote = optional $ symbol "\"" diff --git a/test/Cocoapods/PodfileLockSpec.hs b/test/Cocoapods/PodfileLockSpec.hs index de74cd95e..6c6aaecc9 100644 --- a/test/Cocoapods/PodfileLockSpec.hs +++ b/test/Cocoapods/PodfileLockSpec.hs @@ -3,81 +3,135 @@ module Cocoapods.PodfileLockSpec ( ) where import Data.Map.Strict qualified as Map +import Data.Text (Text) import Data.Text.IO qualified as TIO +import Data.Void (Void) import DepTypes import GraphUtil import Strategy.Cocoapods.PodfileLock import Test.Hspec qualified as T -import Text.Megaparsec +import Test.Hspec.Megaparsec (shouldParse) +import Text.Megaparsec (Parsec, errorBundlePretty, parse, runParser) -dependencyOne :: Dependency -dependencyOne = +parseMatch :: (Show a, Eq a) => Parsec Void Text a -> Text -> a -> T.Expectation +parseMatch parser input expected = parse parser "" input `shouldParse` expected + +depOf :: Text -> Maybe Text -> Dependency +depOf name version = Dependency { dependencyType = PodType - , dependencyName = "one" - , dependencyVersion = Just (CEq "1.0.0") + , dependencyName = name + , dependencyVersion = CEq <$> version , dependencyLocations = [] , dependencyEnvironments = [] , dependencyTags = Map.empty } +dependencyOne :: Dependency +dependencyOne = depOf "one" (Just "1.0.0") + dependencyTwo :: Dependency -dependencyTwo = - Dependency - { dependencyType = PodType - , dependencyName = "two" - , dependencyVersion = Just (CEq "2.0.0") - , dependencyLocations = [] - , dependencyEnvironments = [] - , dependencyTags = Map.empty - } +dependencyTwo = depOf "two" (Just "2.0.0") dependencyThree :: Dependency -dependencyThree = - Dependency - { dependencyType = PodType - , dependencyName = "three" - , dependencyVersion = Just (CEq "3.0.0") - , dependencyLocations = [] - , dependencyEnvironments = [] - , dependencyTags = Map.empty - } +dependencyThree = depOf "three" (Just "3.0.0") dependencyFour :: Dependency -dependencyFour = - Dependency - { dependencyType = PodType - , dependencyName = "four" - , dependencyVersion = Just (CEq "4.0.0") - , dependencyLocations = [] - , dependencyEnvironments = [] - , dependencyTags = Map.empty - } +dependencyFour = depOf "four" (Just "4.0.0") + +dependencyAbnormalName :: Dependency +dependencyAbnormalName = depOf "ab-normal/+name" (Just "2.0.0") + +dependencyNotSoSafeName :: Dependency +dependencyNotSoSafeName = depOf "not-so-safe-name" (Just "2.0.0") + +dependencyGitSourced :: Dependency +dependencyGitSourced = depOf "some-dep-sourced-from-git" (Just "2.0.0") + +dependencyGitTagged :: Dependency +dependencyGitTagged = depOf "depWithTag" (Just "2.0.0") + +dependencyTwoDepA :: Dependency +dependencyTwoDepA = depOf "two_dep_A" Nothing + +dependencyTwoDepB :: Dependency +dependencyTwoDepB = depOf "two-dep-B" Nothing podSection :: Section -podSection = PodSection [Pod "one" "1.0.0" [Dep "two", Dep "three"], Pod "two" "2.0.0" [], Pod "three" "3.0.0" [Dep "four"], Pod "four" "4.0.0" []] +podSection = + PodSection + [ Pod "one" "1.0.0" [Dep "two", Dep "three", Dep "ab-normal/+name"] + , Pod "two" "2.0.0" [Dep "two_dep_A", Dep "two-dep-B"] + , Pod "three" "3.0.0" [Dep "four"] + , Pod "ab-normal/+name" "2.0.0" [] + , Pod "four" "4.0.0" [] + , Pod "not-so-safe-name" "2.0.0" [] + , Pod "some-dep-sourced-from-git" "2.0.0" [] + , Pod "depWithTag" "2.0.0" [] + ] dependencySection :: Section -dependencySection = DependencySection [Dep "one", Dep "three"] +dependencySection = + DependencySection + [ Dep "one" + , Dep "three" + , Dep "not-so-safe-name" + , Dep "some-dep-sourced-from-git" + , Dep "depWithTag" + ] spec :: T.Spec spec = do T.describe "podfile lock analyzer" $ T.it "produces the expected output" $ do let graph = buildGraph [podSection, dependencySection] - - expectDeps [dependencyOne, dependencyTwo, dependencyThree, dependencyFour] graph - expectDirect [dependencyOne, dependencyThree] graph + expectDeps + [ dependencyOne + , dependencyTwo + , dependencyThree + , dependencyAbnormalName + , dependencyFour + , dependencyNotSoSafeName + , dependencyGitSourced + , dependencyGitTagged + , dependencyTwoDepA + , dependencyTwoDepB + ] + graph + expectDirect + [ dependencyOne + , dependencyThree + , dependencyNotSoSafeName + , dependencyGitSourced + , dependencyGitTagged + ] + graph expectEdges - [ (dependencyOne, dependencyTwo) + [ (dependencyOne, dependencyAbnormalName) + , (dependencyOne, dependencyTwo) , (dependencyOne, dependencyThree) + , (dependencyTwo, dependencyTwoDepA) + , (dependencyTwo, dependencyTwoDepB) , (dependencyThree, dependencyFour) ] graph podLockFile <- T.runIO (TIO.readFile "test/Cocoapods/testdata/Podfile.lock") - T.describe "podfile lock parser" $ - T.it "parses error messages into an empty list" $ + T.describe "podfile lock parser" $ do + T.describe "dep parser" $ do + let shouldParseInto input = parseMatch depParser input + T.it "parses names" $ do + "- atomFire (1.0.0)" `shouldParseInto` Dep "atomFire" + "- \"at/+om (1.0.0)\"" `shouldParseInto` Dep "at/+om" + "- \"not-so-safe-name (2.0.0)\"" `shouldParseInto` Dep "not-so-safe-name" + + T.describe "pod parser" $ do + let shouldParseInto input = parseMatch podParser input + T.it "parses names" $ do + "- atomFire (1.0.0)" `shouldParseInto` Pod "atomFire" "1.0.0" ([]) + "- \"atomFire (1.0.0)\"" `shouldParseInto` Pod "atomFire" "1.0.0" ([]) + + T.it "parses pod and dependency sections" $ case runParser findSections "" podLockFile of Left err -> T.expectationFailure ("failed to parse: " <> errorBundlePretty err) Right result -> do diff --git a/test/Cocoapods/testdata/Podfile.lock b/test/Cocoapods/testdata/Podfile.lock index 94b2dd8ae..a16c1dc18 100644 --- a/test/Cocoapods/testdata/Podfile.lock +++ b/test/Cocoapods/testdata/Podfile.lock @@ -2,14 +2,24 @@ PODS: - one (1.0.0): - two (= 3.2.1) - three (= 3.2.1) + - "ab-normal/+name (2.0.0)" - two (2.0.0) + - two_dep_A + - "two-dep-B" - three (3.0.0) - four (= 2.3.3) + - "ab-normal/+name (2.0.0)" - four (4.0.0): + - "not-so-safe-name (2.0.0)" + - some-dep-sourced-from-git (2.0.0) + - depWithTag (2.0.0) DEPENDENCIES: - one (> 4.4) - three (from `Submodules/subproject/.podspec`) + - "not-so-safe-name (2.0.0)" + - "some-dep-sourced-from-git (from `git@github.example.com:ab/ios.git`, commit `559e6a`)" + - depWithTag (from `git@github.example.com:ab/cap.git`, tag `v1.2.3`) SPEC REPOS: https://github.com/cocoapods/specs.git: