fix: parsing of the for parameter of plan media type (#4005)

This commit is contained in:
Taimoor Zaeem
2025-04-12 06:10:25 -05:00
committed by GitHub
parent 6b4648d2e4
commit a6e81a5241
2 changed files with 52 additions and 28 deletions
+1
View File
@@ -34,6 +34,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #3697, #3602, Handle queries on non-existing table gracefully - @taimoorzaeem - #3697, #3602, Handle queries on non-existing table gracefully - @taimoorzaeem
- #3600, #3926, Improve JWT errors - @taimoorzaeem - #3600, #3926, Improve JWT errors - @taimoorzaeem
- #3013, Fix `order=` with POST, PATCH, PUT and DELETE requests - @taimoorzaeem - #3013, Fix `order=` with POST, PATCH, PUT and DELETE requests - @taimoorzaeem
- #3498, Fix incorrect parsing of the `for` parameter of the `application/vnd.pgrst.plan` media type - @taimoorzaeem
### Changed ### Changed
+47 -24
View File
@@ -1,7 +1,7 @@
{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE DuplicateRecordFields #-}
{-# OPTIONS_GHC -Wno-unused-do-bind #-}
module PostgREST.MediaType module PostgREST.MediaType
( MediaType(..) ( MediaType(..)
, MTVndPlanOption (..) , MTVndPlanOption (..)
@@ -13,14 +13,13 @@ module PostgREST.MediaType
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import qualified Data.Text as T
import Network.HTTP.Types.Header (Header, hContentType) import qualified Text.ParserCombinators.Parsec as P
import Data.Map (fromList, (!?)) import Data.Map (fromList, (!?))
import qualified Data.Text as T (break, drop, dropWhile,
dropWhileEnd, null, splitOn,
toLower)
import Data.Text.Encoding (decodeLatin1) import Data.Text.Encoding (decodeLatin1)
import Network.HTTP.Types.Header (Header, hContentType)
import Protolude import Protolude
-- | Enumeration of currently supported media types -- | Enumeration of currently supported media types
@@ -104,6 +103,9 @@ toMimePlanFormat PlanText = "text"
-- >>> decodeMediaType "application/vnd.pgrst.plan;for=\"application/json\"" -- >>> decodeMediaType "application/vnd.pgrst.plan;for=\"application/json\""
-- MTVndPlan MTApplicationJSON PlanText [] -- MTVndPlan MTApplicationJSON PlanText []
-- --
-- >>> decodeMediaType "application/vnd.pgrst.plan ; for=\"text/xml\" ; options=analyze"
-- MTVndPlan MTTextXML PlanText [PlanAnalyze]
--
-- >>> decodeMediaType "application/vnd.pgrst.plan+json;for=\"text/csv\"" -- >>> decodeMediaType "application/vnd.pgrst.plan+json;for=\"text/csv\""
-- MTVndPlan MTTextCSV PlanJSON [] -- MTVndPlan MTTextCSV PlanJSON []
-- --
@@ -150,7 +152,10 @@ decodeMediaType mt = decodeMediaType' $ decodeLatin1 mt
("*","*",_) -> MTAny ("*","*",_) -> MTAny
_ -> MTOther mt' _ -> MTOther mt'
where where
(mainType, subType, params') = tokenizeMediaType mt' mediaTypeOrError = P.parse tokenizeMediaType "parsec: tokenizeMediaType failed" $ T.unpack mt'
(mainType, subType, params') = case mediaTypeOrError of
Right mt'' -> mt''
Left _ -> ("*", "*", []) -- TODO: Throw mediatype error, would need refactoring because currently Error module depend on MediaType module
params = fromList $ map (first T.toLower) params' -- normalize parameter names to lowercase, per RFC 7321 params = fromList $ map (first T.toLower) params' -- normalize parameter names to lowercase, per RFC 7321
getPlan fmt = MTVndPlan mtFor fmt $ getPlan fmt = MTVndPlan mtFor fmt $
[PlanAnalyze | inOpts "analyze" ] ++ [PlanAnalyze | inOpts "analyze" ] ++
@@ -166,21 +171,39 @@ decodeMediaType mt = decodeMediaType' $ decodeLatin1 mt
checkArrayNullStrip = if strippedNulls then MTVndArrayJSONStrip else MTApplicationJSON checkArrayNullStrip = if strippedNulls then MTVndArrayJSONStrip else MTApplicationJSON
-- | Split a Media Type string into components -- | Split a Media Type string into components
-- >>> tokenizeMediaType "application/vnd.pgrst.plan+json;for=\"text/csv\"" -- >>> P.parse tokenizeMediaType "" "application/vnd.pgrst.plan+json;for=\"text/csv\""
-- ("application","vnd.pgrst.plan+json",[("for","text/csv")]) -- Right ("application","vnd.pgrst.plan+json",[("for","text/csv")])
-- >>> tokenizeMediaType "*/*" --
-- ("*","*",[]) -- >>> P.parse tokenizeMediaType "" "*/*"
-- >>> tokenizeMediaType "application/vnd.pgrst.plan;wat=\"application/json;text/csv\"" -- Right ("*","*",[])
-- ("application","vnd.pgrst.plan",[("wat","application/json"),("text/csv\"","")]) --
tokenizeMediaType :: Text -> (Text, Text, [(Text, Text)]) -- >>> P.parse tokenizeMediaType "" "application/vnd.pgrst.plan;wat=\"application/json;text/csv\""
tokenizeMediaType t = (mainType, subType, params) -- Right ("application","vnd.pgrst.plan",[("wat","application/json;text/csv")])
--
-- >>> P.parse tokenizeMediaType "" "application/vnd.pgrst.plan+text; for=\"text/xml\"; options=analyze|verbose|settings|buffers|wal"
-- Right ("application","vnd.pgrst.plan+text",[("for","text/xml"),("options","analyze|verbose|settings|buffers|wal")])
tokenizeMediaType :: P.Parser (Text, Text, [(Text, Text)])
tokenizeMediaType = do
mainType <- P.many1 (P.alphaNum <|> P.oneOf ".*")
P.char '/'
subType <- P.many1 (P.alphaNum <|> P.oneOf ".*+-")
params <- P.many pSemicolonSeparatedKeyVals
P.optional $ P.try $ P.spaces *> P.char ';' -- ending semicolon
P.eof
return (T.pack mainType, T.pack subType, params)
where where
(mainType, rest) = T.break (== '/') t pSemicolonSeparatedKeyVals :: P.Parser (Text, Text)
(subType, restParams) = T.break (== ';') $ T.drop 1 rest pSemicolonSeparatedKeyVals = P.try $ P.spaces *> P.char ';' *> P.spaces *> pKeyVal
params = where
let rp = T.drop 1 restParams pKeyVal :: P.Parser (Text, Text)
in if T.null rp then [] else map param $ T.splitOn ";" rp -- FIXME: breaks if there's a ';' in a quoted value pKeyVal = do
param p = key <- P.many1 P.alphaNum
let (k, v) = T.break (== '=') p P.spaces
in (k, dropAround (== '"') $ T.drop 1 v) -- FIXME: doesn't unescape quotes in values P.char '='
dropAround p = T.dropWhile p . T.dropWhileEnd p P.spaces
val <- P.try pQuoted <|> P.try pUnQuoted
return (T.pack key, T.pack val)
where
pUnQuoted = P.many1 (P.alphaNum <|> P.oneOf "|")
pQuoted = P.char '\"' *> P.manyTill P.anyChar (P.char '\"')