Skip to content

Commit a6e81a5

Browse files
authored
fix: parsing of the for parameter of plan media type (#4005)
1 parent 6b4648d commit a6e81a5

File tree

2 files changed

+52
-28
lines changed

2 files changed

+52
-28
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
3434
- #3697, #3602, Handle queries on non-existing table gracefully - @taimoorzaeem
3535
- #3600, #3926, Improve JWT errors - @taimoorzaeem
3636
- #3013, Fix `order=` with POST, PATCH, PUT and DELETE requests - @taimoorzaeem
37+
- #3498, Fix incorrect parsing of the `for` parameter of the `application/vnd.pgrst.plan` media type - @taimoorzaeem
3738

3839
### Changed
3940

src/PostgREST/MediaType.hs

+51-28
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{-# LANGUAGE DeriveAnyClass #-}
22
{-# LANGUAGE DeriveGeneric #-}
33
{-# LANGUAGE DuplicateRecordFields #-}
4-
4+
{-# OPTIONS_GHC -Wno-unused-do-bind #-}
55
module PostgREST.MediaType
66
( MediaType(..)
77
, MTVndPlanOption (..)
@@ -11,17 +11,16 @@ module PostgREST.MediaType
1111
, decodeMediaType
1212
) where
1313

14-
import qualified Data.Aeson as JSON
15-
import qualified Data.ByteString as BS
14+
import qualified Data.Aeson as JSON
15+
import qualified Data.ByteString as BS
16+
import qualified Data.Text as T
17+
import qualified Text.ParserCombinators.Parsec as P
1618

19+
import Data.Map (fromList, (!?))
20+
import Data.Text.Encoding (decodeLatin1)
1721
import Network.HTTP.Types.Header (Header, hContentType)
1822

19-
import Data.Map (fromList, (!?))
20-
import qualified Data.Text as T (break, drop, dropWhile,
21-
dropWhileEnd, null, splitOn,
22-
toLower)
23-
import Data.Text.Encoding (decodeLatin1)
24-
import Protolude
23+
import Protolude
2524

2625
-- | Enumeration of currently supported media types
2726
data MediaType
@@ -104,6 +103,9 @@ toMimePlanFormat PlanText = "text"
104103
-- >>> decodeMediaType "application/vnd.pgrst.plan;for=\"application/json\""
105104
-- MTVndPlan MTApplicationJSON PlanText []
106105
--
106+
-- >>> decodeMediaType "application/vnd.pgrst.plan ; for=\"text/xml\" ; options=analyze"
107+
-- MTVndPlan MTTextXML PlanText [PlanAnalyze]
108+
--
107109
-- >>> decodeMediaType "application/vnd.pgrst.plan+json;for=\"text/csv\""
108110
-- MTVndPlan MTTextCSV PlanJSON []
109111
--
@@ -150,7 +152,10 @@ decodeMediaType mt = decodeMediaType' $ decodeLatin1 mt
150152
("*","*",_) -> MTAny
151153
_ -> MTOther mt'
152154
where
153-
(mainType, subType, params') = tokenizeMediaType mt'
155+
mediaTypeOrError = P.parse tokenizeMediaType "parsec: tokenizeMediaType failed" $ T.unpack mt'
156+
(mainType, subType, params') = case mediaTypeOrError of
157+
Right mt'' -> mt''
158+
Left _ -> ("*", "*", []) -- TODO: Throw mediatype error, would need refactoring because currently Error module depend on MediaType module
154159
params = fromList $ map (first T.toLower) params' -- normalize parameter names to lowercase, per RFC 7321
155160
getPlan fmt = MTVndPlan mtFor fmt $
156161
[PlanAnalyze | inOpts "analyze" ] ++
@@ -166,21 +171,39 @@ decodeMediaType mt = decodeMediaType' $ decodeLatin1 mt
166171
checkArrayNullStrip = if strippedNulls then MTVndArrayJSONStrip else MTApplicationJSON
167172

168173
-- | Split a Media Type string into components
169-
-- >>> tokenizeMediaType "application/vnd.pgrst.plan+json;for=\"text/csv\""
170-
-- ("application","vnd.pgrst.plan+json",[("for","text/csv")])
171-
-- >>> tokenizeMediaType "*/*"
172-
-- ("*","*",[])
173-
-- >>> tokenizeMediaType "application/vnd.pgrst.plan;wat=\"application/json;text/csv\""
174-
-- ("application","vnd.pgrst.plan",[("wat","application/json"),("text/csv\"","")])
175-
tokenizeMediaType :: Text -> (Text, Text, [(Text, Text)])
176-
tokenizeMediaType t = (mainType, subType, params)
177-
where
178-
(mainType, rest) = T.break (== '/') t
179-
(subType, restParams) = T.break (== ';') $ T.drop 1 rest
180-
params =
181-
let rp = T.drop 1 restParams
182-
in if T.null rp then [] else map param $ T.splitOn ";" rp -- FIXME: breaks if there's a ';' in a quoted value
183-
param p =
184-
let (k, v) = T.break (== '=') p
185-
in (k, dropAround (== '"') $ T.drop 1 v) -- FIXME: doesn't unescape quotes in values
186-
dropAround p = T.dropWhile p . T.dropWhileEnd p
174+
-- >>> P.parse tokenizeMediaType "" "application/vnd.pgrst.plan+json;for=\"text/csv\""
175+
-- Right ("application","vnd.pgrst.plan+json",[("for","text/csv")])
176+
--
177+
-- >>> P.parse tokenizeMediaType "" "*/*"
178+
-- Right ("*","*",[])
179+
--
180+
-- >>> P.parse tokenizeMediaType "" "application/vnd.pgrst.plan;wat=\"application/json;text/csv\""
181+
-- Right ("application","vnd.pgrst.plan",[("wat","application/json;text/csv")])
182+
--
183+
-- >>> P.parse tokenizeMediaType "" "application/vnd.pgrst.plan+text; for=\"text/xml\"; options=analyze|verbose|settings|buffers|wal"
184+
-- Right ("application","vnd.pgrst.plan+text",[("for","text/xml"),("options","analyze|verbose|settings|buffers|wal")])
185+
186+
tokenizeMediaType :: P.Parser (Text, Text, [(Text, Text)])
187+
tokenizeMediaType = do
188+
mainType <- P.many1 (P.alphaNum <|> P.oneOf ".*")
189+
P.char '/'
190+
subType <- P.many1 (P.alphaNum <|> P.oneOf ".*+-")
191+
params <- P.many pSemicolonSeparatedKeyVals
192+
P.optional $ P.try $ P.spaces *> P.char ';' -- ending semicolon
193+
P.eof
194+
return (T.pack mainType, T.pack subType, params)
195+
where
196+
pSemicolonSeparatedKeyVals :: P.Parser (Text, Text)
197+
pSemicolonSeparatedKeyVals = P.try $ P.spaces *> P.char ';' *> P.spaces *> pKeyVal
198+
where
199+
pKeyVal :: P.Parser (Text, Text)
200+
pKeyVal = do
201+
key <- P.many1 P.alphaNum
202+
P.spaces
203+
P.char '='
204+
P.spaces
205+
val <- P.try pQuoted <|> P.try pUnQuoted
206+
return (T.pack key, T.pack val)
207+
where
208+
pUnQuoted = P.many1 (P.alphaNum <|> P.oneOf "|")
209+
pQuoted = P.char '\"' *> P.manyTill P.anyChar (P.char '\"')

0 commit comments

Comments
 (0)