packages feed

figment-0.2.0.0: src/Figment/Provider/Yaml.hs

-- | YAML, read directly from libyaml's event stream.
--
-- * YAML 1.2 core schema: only @true@/@false@ are booleans, so @no@,
--   @on@ and @NO@ (the \"Norway problem\") stay strings.
-- * Every value carries its line and column.
-- * Anchors and aliases are supported, and alias expansion is charged
--   against the node limit, which defeats \"billion laughs\".
-- * Merge keys (@<<: *base@) are supported; explicit keys win.
-- * Duplicate keys are errors; multiple documents are rejected.
module Figment.Provider.Yaml
  ( yaml
  , parseYaml
  ) where

import Control.Applicative ((<|>))
import Control.Exception (try)
import Data.ByteString (ByteString)
import Data.Char (isDigit, isHexDigit, isOctDigit)
import Data.Conduit (runConduitRes, (.|))
import qualified Data.Conduit.List as CL
import qualified Data.Map.Strict as Map
import Data.Map.Strict (Map)
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Encoding as TE
import qualified Data.Vector as Vector
import Numeric (readHex, readOct)
import qualified Text.Libyaml as Y

import Figment.Error
import Figment.Internal.ValueParser (parseNumber)
import Figment.Limits
import Figment.Metadata (Pos (..))
import Figment.Path
import Figment.Provider.Format (Format (..))
import Figment.Value hiding (null)

-- | The YAML format.
yaml :: Format
yaml = Format "YAML" parseYaml

-- | Parse a YAML document, enforcing the limits.
parseYaml :: Limits -> ByteString -> IO (Either ConfigError Value)
parseYaml lim bs = do
  r <- try (runConduitRes (Y.decodeMarked bs .| CL.consume))
  pure $ case r of
    Left (Y.YamlException msg) -> Left (configError (SyntaxError (Text.pack msg)))
    Left (Y.YamlParseException problem ctx mark) ->
      Left (withPos (Just (markPos mark))
        (configError (SyntaxError (Text.pack (problem ++ (if null ctx then "" else " " ++ ctx))))))
    Right [] -> Right emptyDict
    Right events -> fst <$> runY document (YState events Map.empty (maxNodes lim)) lim

markPos :: Y.YamlMark -> Pos
markPos m = Pos (Y.yamlLine m + 1) (Y.yamlColumn m + 1)

------------------------------------------------------------------------
-- A small state/error monad over the event list

data YState = YState
  { ysEvents :: [Y.MarkedEvent]
  , ysAnchors :: Map String Anchored
  , ysBudget :: !Int
  }

-- | An anchored subtree, with its expanded size and container height.
data Anchored = Anchored Value !Int !Int

newtype YP a = YP {runY :: YState -> Limits -> Either ConfigError (a, YState)}

instance Functor YP where
  fmap f (YP g) = YP $ \s l -> fmap (\(a, s') -> (f a, s')) (g s l)

instance Applicative YP where
  pure a = YP $ \s _ -> Right (a, s)
  YP f <*> YP g = YP $ \s l -> case f s l of
    Left e -> Left e
    Right (h, s') -> fmap (\(a, s'') -> (h a, s'')) (g s' l)

instance Monad YP where
  YP g >>= k = YP $ \s l -> case g s l of
    Left e -> Left e
    Right (a, s') -> runY (k a) s' l

failY :: ConfigError -> YP a
failY e = YP $ \_ _ -> Left e

next :: YP (Maybe Y.MarkedEvent)
next = YP $ \s _ -> case ysEvents s of
  [] -> Right (Nothing, s)
  (e : es) -> Right (Just e, s {ysEvents = es})

peek :: YP (Maybe Y.Event)
peek = YP $ \s _ -> Right (Y.yamlEvent <$> safeHead (ysEvents s), s)
  where
    safeHead (x : _) = Just x
    safeHead [] = Nothing

spendY :: Int -> Path -> Pos -> YP ()
spendY k p at = YP $ \s l ->
  let b = ysBudget s - k
  in if b < 0
       then Left (withPos (Just at) (atPath p (configError (TooManyNodes (maxNodes l)))))
       else Right ((), s {ysBudget = b})

depthCheck :: Int -> Path -> Pos -> YP ()
depthCheck d p at = YP $ \s l ->
  if d > maxDepth l
    then Left (withPos (Just at) (atPath p (configError (DepthExceeded (maxDepth l)))))
    else Right ((), s)

remember :: Y.Anchor -> Anchored -> YP ()
remember Nothing _ = pure ()
remember (Just name) a = YP $ \s _ -> Right ((), s {ysAnchors = Map.insert name a (ysAnchors s)})

recall :: String -> YP (Maybe Anchored)
recall name = YP $ \s _ -> Right (Map.lookup name (ysAnchors s), s)

syntaxAt :: Path -> Pos -> Text -> YP a
syntaxAt p at msg = failY (withPos (Just at) (atPath p (configError (SyntaxError msg))))

------------------------------------------------------------------------
-- Grammar

document :: YP Value
document = do
  _ <- expectStart
  ev <- peek
  case ev of
    Just Y.EventStreamEnd -> pure emptyDict
    Just Y.EventDocumentStart -> do
      _ <- next
      Anchored v _ _ <- node 1 root
      end <- next
      case Y.yamlEvent <$> end of
        Just Y.EventDocumentEnd -> pure ()
        _ -> failY (configError (SyntaxError "malformed YAML document"))
      rest <- peek
      case rest of
        Just Y.EventDocumentStart ->
          failY (configError (SyntaxError "multiple YAML documents are not supported in a configuration file"))
        _ -> pure v
    _ -> failY (configError (SyntaxError "malformed YAML stream"))
  where
    expectStart = next

-- | Parse one node at nesting level @d@ (root = 1). Returns the value
-- with its expanded size and container height.
node :: Int -> Path -> YP Anchored
node d p = do
  me <- next
  case me of
    Nothing -> failY (atPath p (configError (SyntaxError "unexpected end of YAML input")))
    Just (Y.MarkedEvent ev start _) -> do
      let at = markPos start
      case ev of
        Y.EventScalar bytes tag style anchor -> do
          spendY 1 p at
          n <- scalar p at bytes tag style
          let a = Anchored (Value untagged {tagPos = Just at} n) 1 0
          remember anchor a
          pure a
        Y.EventSequenceStart _ _ anchor -> do
          spendY 1 p at
          depthCheck d p at
          (items, size, height) <- sequenceItems 0 [] 1 0
          let a = Anchored (Value untagged {tagPos = Just at} (Array (Vector.fromList items))) size (height + 1)
          remember anchor a
          pure a
        Y.EventMappingStart _ _ anchor -> do
          spendY 1 p at
          depthCheck d p at
          (m, size, height) <- mappingPairs Map.empty [] 1 0
          let a = Anchored (Value untagged {tagPos = Just at} (Dict m)) size (height + 1)
          remember anchor a
          pure a
        Y.EventAlias name -> do
          found <- recall name
          case found of
            Nothing -> syntaxAt p at ("unknown alias `*" <> Text.pack name <> "`")
            Just a@(Anchored _ size height) -> do
              spendY size p at
              if height > 0 then depthCheck (d + height - 1) p at else pure ()
              pure a
        _ -> syntaxAt p at "unexpected YAML event"
  where
    sequenceItems :: Int -> [Value] -> Int -> Int -> YP ([Value], Int, Int)
    sequenceItems i acc size height = do
      ev <- peek
      case ev of
        Just Y.EventSequenceEnd -> next >> pure (reverse acc, size, height)
        _ -> do
          Anchored v s h <- node (d + 1) (p /# i)
          sequenceItems (i + 1) (v : acc) (size + s) (max height h)

    -- explicit pairs, then merge-key sources (applied underneath)
    mappingPairs :: Dict -> [Dict] -> Int -> Int -> YP (Dict, Int, Int)
    mappingPairs acc merges size height = do
      me <- next
      case me of
        Just (Y.MarkedEvent Y.EventMappingEnd _ _) ->
          pure (Map.union acc (Map.unions merges), size, height)
        Just (Y.MarkedEvent (Y.EventScalar bytes _ style _) kstart _) -> do
          let kat = markPos kstart
          key <- decodeText p kat bytes
          if key == "<<" && style `elem` [Y.Plain, Y.PlainNoTag, Y.Any]
            then do
              Anchored v s h <- node (d + 1) (p /. key)
              srcs <- mergeSources kat v
              mappingPairs acc (merges ++ srcs) (size + s) (max height (h - 1))
            else
              if Map.member key acc
                then failY (withPos (Just kat) (atPath (p /. key) (configError (DuplicateKey key))))
                else do
                  Anchored v s h <- node (d + 1) (p /. key)
                  mappingPairs (Map.insert key v acc) merges (size + s) (max height h)
        Just (Y.MarkedEvent _ kstart _) ->
          syntaxAt p (markPos kstart) "only scalar mapping keys are supported"
        Nothing -> failY (atPath p (configError (SyntaxError "unexpected end of YAML input")))

    mergeSources kat v = case valueNode v of
      Dict m -> pure [m]
      Array xs | all isDict (Vector.toList xs) -> pure [m | Value _ (Dict m) <- Vector.toList xs]
      _ -> syntaxAt (p /. "<<") kat "a merge key (`<<`) needs a mapping or a list of mappings"
    isDict (Value _ (Dict _)) = True
    isDict _ = False

decodeText :: Path -> Pos -> ByteString -> YP Text
decodeText p at bytes = case TE.decodeUtf8' bytes of
  Right t -> pure t
  Left _ -> syntaxAt p at "invalid UTF-8 in YAML scalar"

-- | Resolve a scalar under the YAML 1.2 core schema.
scalar :: Path -> Pos -> ByteString -> Y.Tag -> Y.Style -> YP Node
scalar p at bytes tag style = do
  t <- decodeText p at bytes
  let quoted = style `elem` [Y.SingleQuoted, Y.DoubleQuoted, Y.Literal, Y.Folded]
      bad what = syntaxAt p at ("`" <> t <> "` is not a valid " <> what)
  case tag of
    Y.StrTag -> pure (String t)
    Y.NullTag -> pure Null
    Y.BoolTag -> maybe (bad "boolean") (pure . Bool) (coreBool t)
    Y.IntTag -> maybe (bad "integer") (pure . Num) (coreInt t)
    Y.FloatTag -> maybe (bad "float") (pure . Num) (coreFloat t <|> coreInt t)
    _ | quoted -> pure (String t)
      | otherwise -> pure (resolve t)
  where
    resolve t
      | t `elem` ["", "~", "null", "Null", "NULL"] = Null
      | Just b <- coreBool t = Bool b
      | Just n <- coreInt t = Num n
      | Just n <- coreFloat t = Num n
      | otherwise = String t

coreBool :: Text -> Maybe Bool
coreBool t
  | t `elem` ["true", "True", "TRUE"] = Just True
  | t `elem` ["false", "False", "FALSE"] = Just False
  | otherwise = Nothing

coreInt :: Text -> Maybe Number
coreInt t
  | Just r <- Text.stripPrefix "0x" t, not (Text.null r), Text.all isHexDigit r =
      Just (Finite (fromInteger (fst (head' (readHex (Text.unpack r))))))
  | Just r <- Text.stripPrefix "0o" t, not (Text.null r), Text.all isOctDigit r =
      Just (Finite (fromInteger (fst (head' (readOct (Text.unpack r))))))
  | otherwise =
      let body = maybe t id (Text.stripPrefix "-" t <> Text.stripPrefix "+" t)
      in if not (Text.null body) && Text.all isDigit body then parseNumber t else Nothing
  where
    head' (x : _) = x
    head' [] = (0, "")

coreFloat :: Text -> Maybe Number
coreFloat t
  | t `elem` [".inf", ".Inf", ".INF", "+.inf", "+.Inf", "+.INF"] = Just PosInfinity
  | t `elem` ["-.inf", "-.Inf", "-.INF"] = Just NegInfinity
  | t `elem` [".nan", ".NaN", ".NAN"] = Just NaN
  | Text.any isDigit t, Text.all (`elem` ("0123456789+-.eE" :: String)) t =
      parseNumber (fixLeadingDot t)
  | otherwise = Nothing
  where
    fixLeadingDot x = case Text.uncons x of
      Just ('.', _) -> "0" <> x
      Just (s, r) | s `elem` ("+-" :: String), Just ('.', _) <- Text.uncons r -> Text.singleton s <> "0" <> r
      _ -> x