packages feed

cuddle-1.2.0.0: bin/Main.hs

{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ViewPatterns #-}

module Main (main) where

import Codec.CBOR.Cuddle.CBOR.Gen (generateFromName)
import Codec.CBOR.Cuddle.CBOR.Validator
import Codec.CBOR.Cuddle.CBOR.Validator.Trace (
  Evidenced (..),
  SValidity (..),
  TraceOptions (..),
  prettyValidationTrace,
 )
import Codec.CBOR.Cuddle.CDDL (CDDL, Name (..), fromRules, sortCDDL)
import Codec.CBOR.Cuddle.CDDL.CTree (CTreeRoot)
import Codec.CBOR.Cuddle.CDDL.Postlude (appendPostlude)
import Codec.CBOR.Cuddle.CDDL.Resolve (
  fullResolveCDDL,
 )
import Codec.CBOR.Cuddle.IndexMappable (IndexMappable (..), mapCDDLDropExt)
import Codec.CBOR.Cuddle.Parser (ParserStage, pCDDL)
import Codec.CBOR.Cuddle.Pretty (PrettyStage)
import Codec.CBOR.FlatTerm (toFlatTerm)
import Codec.CBOR.Pretty (prettyHexEnc)
import Codec.CBOR.Read (deserialiseFromBytes)
import Codec.CBOR.Term (Term, decodeTerm, encodeTerm)
import Codec.CBOR.Write (toStrictByteString)
import Control.Monad (unless)
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.ByteString.Base16 qualified as Base16
import Data.ByteString.Char8 qualified as BSC
import Data.ByteString.Lazy qualified as LBS
import Data.Char (isSpace)
import Data.IORef (newIORef)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Text qualified as T
import Data.Text.Encoding (encodeUtf8)
import Data.Text.IO qualified as T
import Options.Applicative
import Prettyprinter (
  LayoutOptions (..),
  PageWidth (..),
  Pretty (pretty),
  defaultLayoutOptions,
  layoutPretty,
  removeTrailingWhitespace,
 )
import Prettyprinter.Render.Terminal qualified as Ansi
import Prettyprinter.Render.Text qualified as PT
import System.Exit (exitFailure, exitSuccess)
import System.IO (hPutStrLn, stderr)
import System.Random (newStdGen)
import System.Random.Stateful (IOGenM (..), Uniform (..))
import Test.AntiGen (zapAntiGen)
import Test.QuickCheck (Gen)
import Test.QuickCheck.Gen (Gen (..))
import Test.QuickCheck.Random (mkQCGen)
import Text.Megaparsec (ParseErrorBundle, Parsec, errorBundlePretty, runParser)

data Command
  = Format FormatOpts FilePath
  | Validate ValidateOpts FilePath
  | GenerateCBOR GenOpts FilePath
  | ValidateCBOR ValidateCBOROpts FilePath
  | FormatCBOR FormatCBOROpts FilePath

newtype ValidateOpts = ValidateOpts {vNoPrelude :: Bool}

pValidateOpts :: Parser ValidateOpts
pValidateOpts =
  ValidateOpts
    <$> switch
      ( long "no-prelude"
          <> help "Do not include the CDDL prelude."
      )

-- | Various formats for outputtting CBOR
data CBOROutputFormat
  = AsBinary
  | AsHex
  | AsDiagnostic
  | AsTerm
  | AsFlatTerm

outputFormatOptions :: Map String CBOROutputFormat
outputFormatOptions =
  Map.fromList
    [ ("binary", AsBinary)
    , ("hex", AsHex)
    , ("pretty", AsDiagnostic)
    , ("diagnostic", AsDiagnostic)
    , ("term", AsTerm)
    , ("flat", AsFlatTerm)
    ]

pCBOROutputFormat :: ReadM CBOROutputFormat
pCBOROutputFormat = eitherReader $ \k ->
  case Map.lookup k outputFormatOptions of
    Just x -> Right x
    Nothing -> Left k

data GenOpts = GenOpts
  { outputFormat :: CBOROutputFormat
  , outputTo :: Maybe String
  , gNoPrelude :: Bool
  , goSeed :: Maybe Int
  , goSize :: Int
  , goNegative :: Bool
  , itemName :: T.Text
  }

pGenOpts :: Parser GenOpts
pGenOpts =
  GenOpts
    <$> option
      pCBOROutputFormat
      ( long "format"
          <> short 'f'
          <> help "Output format"
          <> value AsHex
          <> completeWith (Map.keys outputFormatOptions)
      )
    <*> optional
      ( strOption
          ( long "out-file"
              <> short 'o'
              <> help "Write to"
          )
      )
    <*> switch
      ( long "no-prelude"
          <> help "Do not include the CDDL prelude."
      )
    <*> optional
      ( option auto $
          long "seed"
            <> short 's'
            <> help "Generator seed"
      )
    <*> option
      auto
      ( long "size"
          <> help "Generator size"
          <> value 30
      )
    <*> switch
      ( long "negative"
          <> short 'n'
          <> help "Generate a negative example"
      )
    <*> argument
      str
      (metavar "RULE" <> help "Name of the CDDL rule to generate a CBOR term for")

newtype FormatOpts = FormatOpts
  {sort :: Bool}

pFormatOpts :: Parser FormatOpts
pFormatOpts =
  FormatOpts
    <$> switch
      ( long "sort-rules"
          <> help "Sort the CDDL rule definitions before printing."
      )

data ValidateCBOROpts = ValidateCBOROpts
  { vcNoPrelude :: Bool
  , vcInputFormat :: CBORInputFormat
  , vcTraceOpts :: TraceOptions
  , vcItemName :: T.Text
  , vcInput :: FilePath
  }

pTraceOpts :: Parser TraceOptions
pTraceOpts =
  TraceOptions
    <$> flag
      True
      False
      ( long "no-fold-valid"
          <> help "Do not squash valid elements in lists/maps when printing validation trace"
      )

pValidateCBOROpts :: Parser ValidateCBOROpts
pValidateCBOROpts =
  ValidateCBOROpts
    <$> switch
      ( long "no-prelude"
          <> help "Do not include the CDDL prelude."
      )
    <*> option
      pCBORInputFormat
      ( long "format"
          <> short 'f'
          <> help "Output format"
          <> completeWith (Map.keys inputFormatOptions)
      )
    <*> pTraceOpts
    <*> argument
      str
      ( metavar "RULE"
          <> help "Name of the CDDL rule to validate this file with"
      )
    <*> argument str (metavar "CBOR_FILE")

data CBORInputFormat
  = FromHex
  | FromBinary

inputFormatOptions :: Map String CBORInputFormat
inputFormatOptions =
  Map.fromList
    [ ("hex", FromHex)
    , ("binary", FromBinary)
    ]

data FormatCBOROpts = FormatCBOROpts
  { dcInputFormat :: CBORInputFormat
  , dcOutputFormat :: CBOROutputFormat
  , dcOutputFile :: Maybe FilePath
  }

pCBORInputFormat :: ReadM CBORInputFormat
pCBORInputFormat = eitherReader $ \k -> case Map.lookup k inputFormatOptions of
  Just x -> Right x
  Nothing -> Left k

pFormatCBOROpts :: Parser FormatCBOROpts
pFormatCBOROpts =
  FormatCBOROpts
    <$> option
      pCBORInputFormat
      ( long "fin"
          <> help "Input format"
          <> value FromBinary
          <> completeWith (Map.keys inputFormatOptions)
      )
    <*> option
      pCBOROutputFormat
      ( long "fout"
          <> help "Output format"
          <> value AsDiagnostic
          <> completeWith (Map.keys outputFormatOptions)
      )
    <*> optional
      ( strOption
          ( long "out-file"
              <> short 'o'
              <> help "Write to"
          )
      )

pCommand :: Parser Command
pCommand =
  subparser
    ( command
        "format"
        ( info
            (Format <$> pFormatOpts <*> argument str (metavar "CDDL_FILE") <**> helper)
            (progDesc "Format the provided CDDL file")
        )
        <> command
          "validate"
          ( info
              (Validate <$> pValidateOpts <*> argument str (metavar "CDDL_FILE") <**> helper)
              (progDesc "Validate the provided CDDL file")
          )
        <> command
          "gen"
          ( info
              (GenerateCBOR <$> pGenOpts <*> argument str (metavar "CDDL_FILE") <**> helper)
              (progDesc "Generate a CBOR term matching the schema")
          )
        <> command
          "validate-cbor"
          ( info
              (ValidateCBOR <$> pValidateCBOROpts <*> argument str (metavar "CDDL_FILE") <**> helper)
              (progDesc "Validate a CBOR file against a schema")
          )
        <> command
          "format-cbor"
          ( info
              (FormatCBOR <$> pFormatCBOROpts <*> argument str (metavar "CBOR_FILE") <**> helper)
              (progDesc "Output a CBOR binary in diagnostic formatting")
          )
    )

main :: IO ()
main = do
  options <-
    execParser $
      info
        (pCommand <**> helper)
        ( fullDesc
            <> progDesc "Manipulate CDDL files"
            <> header "cuddle"
        )
  run options

tryParseFromFile :: FilePath -> IO (CDDL ParserStage)
tryParseFromFile cddlFile =
  parseFromFile pCDDL cddlFile >>= \case
    Left err -> do
      putStrLnErr $ errorBundlePretty err
      exitFailure
    Right res -> pure res

formatTerm :: Term -> CBOROutputFormat -> ByteString
formatTerm term = \case
  AsTerm -> encodeUtf8 . T.pack $ show term
  AsFlatTerm -> encodeUtf8 . T.pack . show . toFlatTerm $ encodeTerm term
  AsBinary -> toStrictByteString $ encodeTerm term
  AsHex -> Base16.encode . toStrictByteString $ encodeTerm term
  AsDiagnostic -> encodeUtf8 . T.pack . prettyHexEnc $ encodeTerm term

runGen :: Int -> Int -> Gen a -> a
runGen seed size gen = unGen gen (mkQCGen seed) size

tryReadCBOR :: CBORInputFormat -> FilePath -> IO ByteString
tryReadCBOR format path = do
  contentsRaw <- BS.readFile path
  case format of
    FromBinary -> pure contentsRaw
    FromHex -> case Base16.decode . BSC.filter (not . isSpace) $ contentsRaw of
      Right x -> pure x
      Left err -> putStrLnErr (show err <> " when decoding hex input") >> exitFailure

run :: Command -> IO ()
run = \case
  Format fOpts cddlFile -> do
    res <- tryParseFromFile cddlFile
    let
      defs
        | sort fOpts = fromRules $ sortCDDL res
        | otherwise = res
      layoutOptions = defaultLayoutOptions {layoutPageWidth = AvailablePerLine 80 1}
      formattedText =
        PT.renderStrict . removeTrailingWhitespace . layoutPretty layoutOptions . pretty $
          mapIndex @_ @_ @PrettyStage defs
    T.putStr formattedText
  Validate vOpts cddlFile -> do
    res <- tryParseFromFile cddlFile
    let
      cddl
        | vNoPrelude vOpts = res
        | otherwise = appendPostlude res
    case fullResolveCDDL $ mapCDDLDropExt cddl of
      Left err -> putStrLnErr (show err) >> exitFailure
      Right _ -> exitSuccess
  GenerateCBOR GenOpts {..} cddlFile -> do
    res <- tryParseFromFile cddlFile
    let
      cddl
        | gNoPrelude = res
        | otherwise = appendPostlude res
    case fullResolveCDDL $ mapCDDLDropExt cddl of
      Left err -> putStrLnErr (show err) >> exitFailure
      Right mt -> do
        seed <- case goSeed of
          Just s -> pure s
          Nothing -> uniformM . IOGenM =<< newIORef =<< newStdGen
        let
          zapN
            | goNegative = 1
            | otherwise = 0
          term = runGen seed goSize . zapAntiGen zapN $ generateFromName (mapIndex mt) (Name itemName)
          formatted = formatTerm term outputFormat
        case outputTo of
          Just outputPath -> BS.writeFile outputPath formatted
          Nothing -> BSC.putStrLn formatted
        putStrLnErr $ "seed: " <> show seed
  ValidateCBOR ValidateCBOROpts {..} cddlFile -> do
    res <- tryParseFromFile cddlFile
    let
      cddl
        | vcNoPrelude = res
        | otherwise = appendPostlude res
    case fullResolveCDDL $ mapCDDLDropExt cddl of
      Left err -> putStrLnErr (show err) >> exitFailure
      Right mt -> do
        cbor <- BS.readFile vcInput
        runValidateCBOR cbor (Name vcItemName) (mapIndex mt) vcTraceOpts
  FormatCBOR FormatCBOROpts {..} cborFile -> do
    contents <- tryReadCBOR dcInputFormat cborFile
    term <- case deserialiseFromBytes decodeTerm (LBS.fromStrict contents) of
      Right (leftover, term) -> do
        unless (LBS.null leftover) . putStrLnErr $
          "Warning: " <> show (LBS.length leftover) <> " leftover bytes after decoding"
        pure term
      Left err -> putStrLnErr (show err) >> exitFailure
    let
      formatted = formatTerm term dcOutputFormat
    case dcOutputFile of
      Just outputPath -> BS.writeFile outputPath formatted
      Nothing -> BSC.putStrLn formatted

putStrLnErr :: String -> IO ()
putStrLnErr = hPutStrLn stderr

parseFromFile ::
  Parsec e T.Text a ->
  String ->
  IO (Either (ParseErrorBundle T.Text e) a)
parseFromFile p file = runParser p file <$> T.readFile file

runValidateCBOR :: BS.ByteString -> Name -> CTreeRoot ValidatorStage -> TraceOptions -> IO ()
runValidateCBOR bs rule cddl traceOpts =
  case validateCBOR bs rule cddl of
    Evidenced validity trc -> do
      T.putStrLn . Ansi.renderStrict . layoutPretty defaultLayoutOptions $
        prettyValidationTrace traceOpts trc
      putStrLn mempty
      case validity of
        SValid -> do
          putStrLn "Validation successful"
          exitSuccess
        SInvalid -> do
          putStrLn "Validation failed"
          exitFailure