packages feed

yamlet-1.0.0.0: tests/Yamlet/Test/Decode/Input.hs

-- | The bytes before parsing: the Unicode encodings, byte order marks, an
-- empty stream, and the file functions.
module Yamlet.Test.Decode.Input
  ( inputTests
  ) where

import Control.Exception
import Data.Bifunctor
import Data.ByteString qualified as BS
import Data.List.NonEmpty qualified as NE
import Data.Map.Strict qualified as M
import Data.Text qualified as T
import Data.Text.Encoding qualified as T
import System.Directory
import System.IO
import Test.Tasty
import Test.Tasty.HUnit

import Yamlet
import Yamlet.Syntax qualified as S
import Yamlet.Test.Helpers

inputTests :: TestTree
inputTests =
  testGroup
    "input"
    [ testCase "empty stream" test_emptyStream
    , testCase "encodings" test_encodings
    , testCase "byte order marks" test_byteOrderMarks
    , testCase "files" test_files
    ]

-- | The file functions write UTF-8 and read back what they wrote.
test_files :: Assertion
test_files = do
  dir <- getTemporaryDirectory
  (path, h) <- openTempFile dir "yamlet.yaml"
  hClose h
  flip finally (removeFile path) $ do
    let value = M.fromList @T.Text @T.Text [("name", "zażółć")]
    encodeFile path value
    bytes <- BS.readFile path
    assertEqual
      "UTF-8"
      (T.encodeUtf8 "name: zażółć\n")
      bytes
    decoded <- decodeFile path
    assertEqual
      "document"
      (Right value)
      decoded
    encodeAllFile @Int path [1, 2]
    documents <- decodeAllFile @Int path
    assertEqual
      "documents"
      (Right [1, 2])
      documents

test_emptyStream :: Assertion
test_emptyStream = do
  assertEqual
    "null"
    (Right Nothing)
    (decodeText @(Maybe Int) "# nothing\n")
  assertEqual
    "all"
    (Right [])
    (decodeAllText @Int "")

test_encodings :: Assertion
test_encodings = do
  let text = "key: zażółć \x1F600\n" :: T.Text
  assertEqual
    "UTF-8 with BOM"
    (Right text)
    (decodeInput ("\xEF\xBB\xBF" <> T.encodeUtf8 text) >>= stripBom)
  assertEqual
    "UTF-16LE"
    (Right text)
    (decodeInput (T.encodeUtf16LE text))
  assertEqual
    "UTF-16BE"
    (Right text)
    (decodeInput (T.encodeUtf16BE text))
  assertEqual
    "UTF-32LE"
    (Right text)
    (decodeInput (T.encodeUtf32LE text))
  assertEqual
    "UTF-32BE"
    (Right text)
    (decodeInput (T.encodeUtf32BE text))
  case decodeInput "a: b\n\xFF\n" of
    Left err ->
      assertEqual
        "invalid UTF-8"
        (2, 1)
        (err.location.line, err.location.column)
    Right _ -> assertFailure "expected an error"
  let stream = "a: 1\n---\n- b\n" :: T.Text
  case S.parseDocumentsText stream of
    Right docs -> do
      assertEqual
        "documents of the stream"
        2
        (length docs)
      assertEqual
        "documents parsed from UTF-16LE"
        (Right docs)
        (S.parseDocuments (T.encodeUtf16LE stream))
    Left err -> assertFailure (show err)
  case S.parseDocuments "a: b\n\xFF\n" of
    Left err ->
      assertEqual
        "documents parsed from invalid UTF-8"
        (2, 1, "invalid UTF-8")
        (err.location.line, err.location.column, err.message)
    Right _ -> assertFailure "expected an error"
  let invalid :: String -> T.Text -> BS.ByteString -> Assertion
      invalid preface msg bytes =
        assertEqual
          preface
          (Just (2, 3, T.unpack msg))
          (errorOf (first pure (decodeInput bytes)))
  invalid
    "lone surrogate in UTF-16LE"
    "invalid UTF-16"
    (T.encodeUtf16LE "a\nbc" <> "\x00\xD8" <> "d\0")
  invalid
    "odd length of UTF-16BE"
    "invalid UTF-16"
    (T.encodeUtf16BE "a\nbc" <> "\0")
  invalid
    "surrogate in UTF-32BE"
    "invalid UTF-32"
    (T.encodeUtf32BE "a\nbc" <> "\0\0\xDC\0")
  invalid
    "code point beyond Unicode in UTF-32LE"
    "invalid UTF-32"
    (T.encodeUtf32LE "a\nbc" <> "\0\0\x11\0")
  invalid
    "incomplete character in UTF-8"
    "invalid UTF-8"
    ("a\nbc" <> "\xE2\x82")
  invalid
    "surrogate in UTF-8"
    "invalid UTF-8"
    ("a\nbc" <> "\xED\xA0\x80" <> "d")
  let column :: String -> Int -> BS.ByteString -> Assertion
      column preface expected bytes =
        assertEqual
          preface
          (Just expected)
          ((\(_, c, _) -> c) <$> errorOf (decode @Value bytes))
  column
    "error after a UTF-8 BOM"
    1
    "\xEF\xBB\xBF]"
  column
    "error after a UTF-16 BOM"
    1
    "\xFF\xFE]\0"
  column
    "invalid UTF-8 after a BOM"
    2
    "\xEF\xBB\xBF\&b\xFF"
  column
    "error after a BOM between documents"
    1
    "a\n...\n\xEF\xBB\xBF]"
  column
    "error after two BOMs"
    1
    "\xEF\xBB\xBF\xEF\xBB\xBF]"
  column
    "error after two BOMs before a marker"
    5
    "a\n\xEF\xBB\xBF\xEF\xBB\xBF--- ]"
  let errorAfterBom :: String -> (Int, Int, String) -> T.Text -> Assertion
      errorAfterBom preface expected input =
        assertEqual
          preface
          (Just expected)
          (errorOf (decodeAllText @Value input))
  errorAfterBom
    "error after a BOM after an end marker"
    (3, 5, "unexpected ':', quote the value if it contains \": \"")
    "a\n...\n\xFEFF\&b: x: y\n"
  errorAfterBom
    "error after a BOM and a comment after an end marker"
    (4, 4, "unterminated flow sequence")
    "a\n...\n\xFEFF# c\n\xFEFF\&b: [\n"
  errorAfterBom
    "error after a second BOM at the start"
    (1, 4, "unterminated flow sequence")
    "\xFEFF\xFEFF\&a: [\n"
  errorAfterBom
    "BOM at the end of a line"
    (1, 6, "unexpected byte order mark")
    "key: \xFEFF\n  sub: x\n"
  errorAfterBom
    "BOM at the end of a line in a flow sequence"
    (1, 2, "unexpected byte order mark")
    "[\xFEFF\nfoo: bar\n]\n"
  errorAfterBom
    "mapping after a BOM and a marker"
    (3, 6, "unexpected ':', a mapping cannot start on the line of '---'")
    "a\n...\n\xFEFF--- c: d\n"
  errorAfterBom
    "list after a BOM and a marker"
    (3, 5, "unexpected '-', a list cannot start on the line of '---'")
    "a\n...\n\xFEFF--- - c\n"
  errorAfterBom
    "tab below a BOM and a comment"
    (2, 2, "unexpected '%', a plain scalar cannot start with it, quote the value")
    "\xFEFF# c\n\t%x\n"
  assertEqual
    "source line after a BOM"
    (Left "]")
    $ either
      (Left . (.sourceLine) . NE.head)
      (const (Right ()))
      (decode @Value "\xEF\xBB\xBF]")
  assertEqual
    "source line at the line feed of a CRLF"
    "a: 1"
    (errorAt "a: 1\r\nb: 2\n" (Offset 5) "message").sourceLine
  where
    stripBom :: T.Text -> Either Error T.Text
    stripBom = Right . T.dropWhile (== '\xFEFF')

test_byteOrderMarks :: Assertion
test_byteOrderMarks = do
  let documents :: String -> [T.Text] -> T.Text -> Assertion
      documents preface expected input =
        assertEqual
          preface
          (Right expected)
          (decodeAllText input)
  documents
    "BOM before a marker after a scalar"
    ["a", "b"]
    "a\n\xFEFF--- b\n"
  assertEqual
    "BOM before a marker after a mapping"
    (Right [Mapping [(String "a", Int 1)], String "b"])
    (decodeAllText @Value "a: 1\n\xFEFF--- b\n")
  documents
    "BOM after an end marker"
    ["a", "b"]
    "a\n...\n\xFEFF# c\n\xFEFF\&b\n"
  documents
    "BOM in a quoted scalar"
    ["a\xFEFF", "b\xFEFF"]
    "--- \"a\xFEFF\"\n--- 'b\xFEFF'\n"
  assertEqual
    "error after a BOM in a quoted scalar"
    (Just (2, 1, "invalid escape sequence, write \\\\ for a backslash or use single quotes"))
    (errorOf (decodeAllText @Value "\"a\n\xFEFF\\q\"\n"))
  assertEqual
    "error below a BOM that starts a document"
    (Just (4, 1, "unexpected key among list items"))
    (errorOf (decodeAllText @Value "x\n...\n\xFEFF- a\nb: c\n"))
  let bom :: String -> (Int, Int) -> T.Text -> Assertion
      bom preface (l, c) input =
        assertEqual
          preface
          (Just (l, c, "unexpected byte order mark"))
          (errorOf (decodeAllText @Value input))
  bom
    "BOM at the start of a key"
    (2, 1)
    "a: 1\n\xFEFF b: 2\n"
  bom
    "BOM at the start of a value"
    (1, 6)
    "key: \xFEFFvalue\n"
  bom
    "BOM in a plain scalar"
    (1, 5)
    "a: x\xFEFFy\n"
  bom
    "BOM in a block scalar"
    (2, 3)
    "a: |\n  \xFEFFx\n"
  bom
    "BOM before a key"
    (2, 1)
    "a: b\n\xFEFF\&c: d\n"
  bom
    "BOM before a list item"
    (2, 1)
    "- a\n\xFEFF- b\n"
  bom
    "BOM before an indented value"
    (2, 1)
    "a:\n\xFEFF  b\n"
  assertEqual
    "BOM before a comment after a mapping"
    (Right [Mapping [(String "a", String "b")]])
    (decodeAllText @Value "a: b\n\xFEFF#c\n")
  documents
    "BOM before a comment after a scalar"
    ["a"]
    "a\n\xFEFF# c\n"
  documents
    "BOM at the end after a scalar"
    ["a"]
    "a\n\xFEFF"
  assertEqual
    "BOM at the end after a marker"
    (Right [Null])
    (decodeAllText @Value "---\n\xFEFF")
  bom
    "BOM before a scalar after a scalar"
    (2, 1)
    "a\n\xFEFF\&b\n"
  bom
    "BOM in a flow sequence"
    (2, 1)
    "a: [x,\n\xFEFF y]\n"
  bom
    "BOM in a flow mapping"
    (2, 1)
    "a: {x: 1,\n\xFEFF\&y: 2}\n"
  bom
    "BOM before a closing bracket"
    (2, 1)
    "a: [x,\n\xFEFF]\n"
  bom
    "BOM before a comment inside a mapping"
    (2, 1)
    "a: 1\n\xFEFF# c\nb: 2\n"
  bom
    "BOM on an empty line inside a mapping"
    (2, 1)
    "a: 1\n\xFEFF\nb: 2\n"
  bom
    "BOM before a comment inside a list"
    (3, 1)
    "a:\n  - 1\n\xFEFF  # c\n  - 2\n"
  bom
    "second BOM line inside a mapping"
    (3, 1)
    "a: 1\n# c\n\xFEFF# d\n\xFEFF\nb: 2\n"
  let errorAfter :: String -> (Int, Int, String) -> T.Text -> Assertion
      errorAfter preface expected input =
        assertEqual
          preface
          (Just expected)
          (errorOf (decodeAllText @Value input))
  errorAfter
    "error after a BOM in a double-quoted scalar"
    (2, 4, "unexpected '@', a plain scalar cannot start with it, quote the value")
    "\"x\n\xFEFFy\" @\n"
  errorAfter
    "error after a BOM in a single-quoted scalar"
    (2, 4, "unexpected 'z' after the end of a quoted scalar")
    "'x\n\xFEFFy' z\n"
  errorAfter
    "error after a BOM in a flow sequence"
    (2, 5, "unexpected '@', a plain scalar cannot start with it, quote the value")
    "[\"x\n\xFEFFy\", @]\n"
  assertEqual
    "BOM before a marker after an unterminated flow sequence"
    (Just (1, 4, "unterminated flow sequence"))
    (errorOf (decodeAllText @Value "a: [x,\n\xFEFF---\nb\n"))
  documents
    "BOM before a start marker in a double-quoted scalar"
    ["a \xFEFF--- "]
    "\"a\n\xFEFF---\n\"\n"
  documents
    "BOM before an end marker in a single-quoted scalar"
    ["a \xFEFF... b"]
    "'a\n\xFEFF... b'\n"
  documents
    "two BOMs before a marker"
    ["a", "b"]
    "a\n\xFEFF\xFEFF--- b\n"
  documents
    "two BOMs before a marker after an end marker"
    ["a", "b"]
    "--- a\n...\n\xFEFF\xFEFF--- b\n"
  documents
    "two BOMs before a marker after a block scalar"
    ["x\n", "b"]
    "--- |\n x\n\xFEFF\xFEFF--- b\n"
  documents
    "BOM before a marker after a literal at the top level"
    ["x\n", "b"]
    "--- |\nx\n\xFEFF--- b\n"
  documents
    "BOM before a marker after a folded at the top level"
    ["x y\n", "b"]
    "--- >\nx\ny\n\xFEFF--- b\n"
  documents
    "BOM before a comment after a literal at the top level"
    ["x\n"]
    "--- |\nx\n\xFEFF# c\n"
  documents
    "BOM before a marker as the first line of a literal"
    ["", "b"]
    "--- |\n\xFEFF--- b\n"
  -- The time to check a run of BOMs is linear in its length.
  documents
    "many BOMs at the start"
    ["a"]
    (T.replicate 400000 "\xFEFF" <> "a\n")
  documents
    "many BOMs after an end marker"
    ["a", "b"]
    ("a\n...\n" <> T.replicate 400000 "\xFEFF" <> "b\n")