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

This commit is contained in:
Taimoor Zaeem
2025-04-12 15:37:37 +00:00
committed by Wolfgang Walther
parent 026d84a093
commit 513704dc2b
3 changed files with 57 additions and 30 deletions
+4
View File
@@ -5,6 +5,10 @@ This project adheres to [Semantic Versioning](http://semver.org/).
## Unreleased ## Unreleased
### Fixed
- #3498, Fix incorrect parsing of the `for` parameter of the `application/vnd.pgrst.plan` media type - @taimoorzaeem
## [12.2.8] - 2025-02-10 ## [12.2.8] - 2025-02-10
### Fixed ### Fixed
+51 -28
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 (..)
@@ -11,17 +11,16 @@ module PostgREST.MediaType
, decodeMediaType , decodeMediaType
) where ) where
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 qualified Text.ParserCombinators.Parsec as P
import Data.Map (fromList, (!?))
import Data.Text.Encoding (decodeLatin1)
import Network.HTTP.Types.Header (Header, hContentType) import Network.HTTP.Types.Header (Header, hContentType)
import Data.Map (fromList, (!?)) import Protolude
import qualified Data.Text as T (break, drop, dropWhile,
dropWhileEnd, null, splitOn,
toLower)
import Data.Text.Encoding (decodeLatin1)
import Protolude
-- | Enumeration of currently supported media types -- | Enumeration of currently supported media types
data MediaType data MediaType
@@ -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")])
where --
(mainType, rest) = T.break (== '/') t -- >>> P.parse tokenizeMediaType "" "application/vnd.pgrst.plan+text; for=\"text/xml\"; options=analyze|verbose|settings|buffers|wal"
(subType, restParams) = T.break (== ';') $ T.drop 1 rest -- Right ("application","vnd.pgrst.plan+text",[("for","text/xml"),("options","analyze|verbose|settings|buffers|wal")])
params =
let rp = T.drop 1 restParams tokenizeMediaType :: P.Parser (Text, Text, [(Text, Text)])
in if T.null rp then [] else map param $ T.splitOn ";" rp -- FIXME: breaks if there's a ';' in a quoted value tokenizeMediaType = do
param p = mainType <- P.many1 (P.alphaNum <|> P.oneOf ".*")
let (k, v) = T.break (== '=') p P.char '/'
in (k, dropAround (== '"') $ T.drop 1 v) -- FIXME: doesn't unescape quotes in values subType <- P.many1 (P.alphaNum <|> P.oneOf ".*+-")
dropAround p = T.dropWhile p . T.dropWhileEnd p 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
pSemicolonSeparatedKeyVals :: P.Parser (Text, Text)
pSemicolonSeparatedKeyVals = P.try $ P.spaces *> P.char ';' *> P.spaces *> pKeyVal
where
pKeyVal :: P.Parser (Text, Text)
pKeyVal = do
key <- P.many1 P.alphaNum
P.spaces
P.char '='
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 '\"')
+2 -2
View File
@@ -103,8 +103,8 @@ postJsonArrayTest(){
echo "Running memory usage tests.." echo "Running memory usage tests.."
jsonKeyTest "1M" "POST" "/rpc/leak?columns=blob" "27M" jsonKeyTest "1M" "POST" "/rpc/leak?columns=blob" "27M"
jsonKeyTest "1M" "POST" "/leak?columns=blob" "20M" jsonKeyTest "1M" "POST" "/leak?columns=blob" "21M"
jsonKeyTest "1M" "PATCH" "/leak?id=eq.1&columns=blob" "20M" jsonKeyTest "1M" "PATCH" "/leak?id=eq.1&columns=blob" "21M"
jsonKeyTest "10M" "POST" "/rpc/leak?columns=blob" "32M" jsonKeyTest "10M" "POST" "/rpc/leak?columns=blob" "32M"
jsonKeyTest "10M" "POST" "/leak?columns=blob" "32M" jsonKeyTest "10M" "POST" "/leak?columns=blob" "32M"