packages feed

telescope-0.2.0: src/Telescope/Asdf/Encoding/File.hs

{-# LANGUAGE RecordWildCards #-}

module Telescope.Asdf.Encoding.File where

import Control.Monad (replicateM_)
import Data.Binary.Get
import Data.Binary.Put
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.ByteString.Char8 qualified as BC
import Data.ByteString.Lazy qualified as BL
import Data.String (IsString)
import Data.Word
import Effectful
import Effectful.Error.Static
import Effectful.Fail
import Effectful.NonDet
import Effectful.State.Static.Local
import Telescope.Asdf.Error
import Telescope.Asdf.Node (Tree)


-- | Split an encoded 'ByteString' into a 'Tree', '[Encoded Block]' and 'Encoded Index'
splitAsdfFile :: (Error AsdfError :> es) => ByteString -> Eff es AsdfFile
splitAsdfFile dat = do
  res <- runFail $ evalState dat parseAsdfFile
  case res of
    Left e -> throwError $ ParseError e
    Right a -> pure a


-- | Lightweight first-pass parsing using 'State ByteString' instead of Parsec or similar
parseAsdfFile :: (State ByteString :> es, Fail :> es) => Eff es AsdfFile
parseAsdfFile = do
  -- if it's empty, then give an error
  tree <- onEmpty "tree" parseTree
  blocks <- onEmpty "blocks" parseBlocks
  index <- parseIndex
  pure $ AsdfFile{tree, blocks, index}
 where
  onEmpty ex eff = do
    res <- runNonDet OnEmptyKeep eff
    case res of
      Left _ -> do
        inp <- get @ByteString
        fail $ "Expected " ++ ex ++ " at " ++ show (BS.take 100 inp)
      Right a -> pure a


parseTree :: (State ByteString :> es, NonDet :> es) => Eff es (Encoded Tree)
parseTree = do
  t <- parseUntil blockMagicToken <|> parseUntil blockIndexHeader <|> remainingBytes
  pure $ Encoded t


parseIndex :: (State ByteString :> es) => Eff es (Encoded Index)
parseIndex =
  Encoded <$> get


parseBlocks :: (State ByteString :> es, NonDet :> es) => Eff es [Encoded Block]
parseBlocks = many parseBlock


parseBlock :: (State ByteString :> es, NonDet :> es) => Eff es (Encoded Block)
parseBlock = do
  exactly blockMagicToken
  b <- parseUntil blockMagicToken <|> parseUntil blockIndexHeader <|> nonEmpty
  case b of
    "" -> empty
    _ -> pure $ Encoded $ blockMagicToken <> b


exactly :: (State ByteString :> es, NonDet :> es) => ByteString -> Eff es ()
exactly val = do
  inp <- get @ByteString
  if val `BS.isPrefixOf` inp
    then do
      put $ BS.drop (BS.length val) inp
      pure ()
    else empty


parseUntil :: (State ByteString :> es, NonDet :> es) => ByteString -> Eff es ByteString
parseUntil val = do
  inp <- get @ByteString
  let (before, rest) = BS.breakSubstring val inp
  case rest of
    "" -> empty
    _ -> do
      put rest
      pure before


nonEmpty :: (NonDet :> es, State ByteString :> es) => Eff es ByteString
nonEmpty = do
  b <- remainingBytes
  case b of
    "" -> empty
    _ -> pure b


remainingBytes :: (NonDet :> es, State ByteString :> es) => Eff es ByteString
remainingBytes = do
  inp <- get
  put @ByteString ""
  pure inp


breakIndex :: (State ByteString :> es) => Eff es (Encoded Index)
breakIndex = Encoded <$> get


concatAsdfFile :: AsdfFile -> ByteString
concatAsdfFile a =
  mconcat [a.tree.bytes, blocks a.blocks, index a.index]
 where
  blocks :: [Encoded Block] -> ByteString
  blocks ebks = mconcat $ fmap (.bytes) ebks
  -- case mconcat $ fmap (.bytes) ebks of
  --   "" -> ""
  --   s -> s <> "\n"
  index ix = ix.bytes


encodeTree :: ByteString -> Encoded Tree
encodeTree tr =
  Encoded $ BS.intercalate "\n" (headers <> formatDoc tr) <> "\n" -- has a trailing newline
 where
  formatDoc doc = ["--- " <> doc, "..."]
  headers = ["#ASDF 1.0.0", "#ASDF_STANDARD 1.5.0", "%YAML 1.1", tagDirective]
  tagDirective = "%TAG ! tag:stsci.edu:asdf/"


encodeBlocks :: [BlockData] -> [Encoded Block]
encodeBlocks = fmap encodeBlock


encodeBlock :: BlockData -> Encoded Block
encodeBlock b =
  Encoded $ BL.toStrict $ runPut $ putBlock b


encodeIndex :: BlockIndex -> Encoded Index
encodeIndex (BlockIndex ns) =
  Encoded $ BS.intercalate "\n" $ ["#ASDF BLOCK INDEX", "%YAML 1.1", "---"] <> fmap indexEntry ns <> ["..."]
 where
  indexEntry n = "- " <> BC.pack (show n)


data Index
data Block


-- | values that have been encoded to the file format: padding, etc
newtype Encoded a = Encoded {bytes :: ByteString}
  deriving (Show, Eq)
  deriving newtype (IsString)


-- | Decompressed block data
newtype BlockData = BlockData {bytes :: ByteString}
  deriving (Eq)


newtype BlockSource = BlockSource Int
  deriving (Eq)


instance Show BlockData where
  show (BlockData bs) = "BlockData " <> show (BS.length bs)


data BlockHeader = BlockHeader
  { headerSize :: Word16
  , flags :: Word32
  , compression :: Compression
  , allocatedSize :: Word64
  , usedSize :: Word64
  , dataSize :: Word64
  , checksum :: Checksum
  }
  deriving (Show, Eq)


newtype BlockIndex = BlockIndex [Int]
  deriving (Eq)


data Compression
  = NoCompression -- "\0\0\0\0"
  | ZLib -- "zlib"
  | BZip2 -- "bzp2"
  deriving (Show, Eq)


newtype Checksum = Checksum ByteString
  deriving (Show, Eq)


noChecksum :: Checksum
noChecksum = Checksum $ BC.replicate 16 '0'


data AsdfFile = AsdfFile
  { tree :: Encoded Tree
  , blocks :: [Encoded Block]
  , index :: Encoded Index
  }
  deriving (Show, Eq)


getBlock :: Get BlockData
getBlock = do
  h <- getBlockHeader
  getBlockData h


-- | Skip along blocks and create a list of all of them
getBlocks :: Get [BlockData]
getBlocks = do
  isBlock <- checkMagicToken
  if not isBlock
    then pure []
    else do
      b <- getBlock
      bs <- getBlocks
      pure (b : bs)


getBlockHeader :: Get BlockHeader
getBlockHeader = do
  expectMagicToken
  headerSize <- label "header_size" getWord16be
  start <- bytesRead

  -- the remainder is inside the headerSize
  flags <- label "flags" getWord32be -- 4
  compression <- label "compression" getCompression -- 4
  allocatedSize <- label "allocated_size" getWord64be -- 8
  usedSize <- label "used_size" getWord64be -- 8
  dataSize <- label "data_size" getWord64be -- 8
  checksum <- label "checksum" getChecksum -- 2
  end <- bytesRead

  -- skip until the end of headerSize
  let usedHead = fromIntegral $ end - start
  skip $ fromIntegral headerSize - usedHead

  pure $ BlockHeader{..}
 where
  getCompression = do
    val <- getByteString 4
    case val of
      "\0\0\0\0" -> pure NoCompression
      -- "zlib" -> pure ZLib
      -- "bzp2" -> pure BZip2
      _ -> fail $ "BlockHeader compression invalid, found " <> show val

  getChecksum = Checksum <$> getByteString 16

  expectMagicToken = do
    m <- getMagicToken
    case m of
      Right a -> pure a
      Left str -> fail $ "BlockHeader magic token invalid: " ++ show str


putBlockHeader :: BlockHeader -> Put
putBlockHeader h = do
  putByteString blockMagicToken
  putWord16be h.headerSize
  size <- putHeaderContent
  let emptyBytes = fromIntegral h.headerSize - size :: Int
  replicateM_ emptyBytes $ putWord8 0x0
 where
  putCompression _ = putByteString "\0\0\0\0"
  putChecksum (Checksum cs) = putByteString cs
  putHeaderContent = do
    let bs = runPut $ do
          putWord32be 0 -- flags
          putCompression NoCompression
          putWord64be h.allocatedSize
          putWord64be h.usedSize
          putWord64be h.dataSize
          putChecksum h.checksum
    putLazyByteString bs
    pure $ fromIntegral $ BL.length bs


blockIndex :: Encoded Tree -> [Encoded Block] -> BlockIndex
blockIndex (Encoded bytes) ebs =
  let ns = scanl go (BS.length bytes) ebs
   in BlockIndex $ take (length ns - 1) ns
 where
  go :: Int -> Encoded Block -> Int
  go n eb = n + BS.length eb.bytes


putBlock :: BlockData -> Put
putBlock bd@(BlockData bs) = do
  putBlockHeader $ blockHeader bd
  putByteString bs


blockHeader :: BlockData -> BlockHeader
blockHeader (BlockData bs) =
  let bytes = fromIntegral $ BS.length bs
   in BlockHeader
        { headerSize = 48 -- minimum allowed size. Our encoding uses fewer bytes than this
        , compression = NoCompression
        , allocatedSize = bytes
        , usedSize = bytes
        , dataSize = bytes
        , checksum = noChecksum
        , flags = 0
        }


getBlockData :: BlockHeader -> Get BlockData
getBlockData h = do
  -- LATER: handle compression
  bytes <- getByteString $ fromIntegral h.usedSize
  _empty <- getByteString $ fromIntegral $ h.allocatedSize - h.usedSize
  pure $ BlockData bytes


blockMagicToken :: ByteString
blockMagicToken = BS.pack [0xd3, 0x42, 0x4c, 0x4b]


blockIndexHeader :: ByteString
blockIndexHeader = "#ASDF BLOCK INDEX"


-- | Verify that the magic token is next without consuming any input
checkMagicToken :: Get Bool
checkMagicToken = do
  emp <- isEmpty
  if emp
    then pure False
    else do
      eb <- lookAhead getMagicToken
      pure $ either (const False) (const True) eb


-- | consume the magic token if available
getMagicToken :: Get (Either ByteString ())
getMagicToken = do
  -- this still fails if it is empty
  str <- getByteString 4
  pure $
    if str == blockMagicToken
      then Right ()
      else Left str