packages feed

asn1-ber-syntax-0.2.0.0: src/Asn/Ber/Encode.hs

{-# language BangPatterns #-}
{-# language DataKinds #-}
{-# language DuplicateRecordFields #-}
{-# language LambdaCase #-}
{-# language MultiWayIf #-}
{-# language NamedFieldPuns #-}
{-# language NumericUnderscores #-}
{-# language TypeApplications #-}

module Asn.Ber.Encode
  ( encode
  ) where

import Prelude hiding (length)

import Asn.Ber (Value(..),Contents(..),Class(..))
import Asn.Oid (Oid(..))
import Control.Monad.ST (runST)
import Data.Bits ((.&.),(.|.),unsafeShiftL,unsafeShiftR,bit,testBit)
import Data.Bytes.Types (Bytes(Bytes))
import Data.ByteString.Short.Internal (ShortByteString(SBS))
import Data.Foldable (foldMap',foldlM)
import Data.Int (Int64)
import Data.Primitive (SmallArray,PrimArray)
import Data.Primitive.ByteArray (byteArrayFromList,ByteArray(ByteArray))
import Data.Word (Word8,Word32)

import qualified Data.Primitive as Prim
import qualified Data.Primitive.Contiguous as C
import qualified Data.Bytes as Bytes
import qualified Data.Bytes.Builder.Bounded as BB
import qualified Data.Bytes.Types
import qualified Data.Text.Short as TS
import qualified Chronos
import qualified Arithmetic.Nat as Nat

data Encoder
  = Leaf {-# UNPACK #-} !Bytes
  | Node
    { _length :: !Int
    , _children :: !(SmallArray Encoder)
    }
  deriving(Show)

length :: Encoder -> Int
length (Leaf bs) = Bytes.length bs
length a@(Node _ _) = _length a

instance Semigroup Encoder where
  a <> b
    | length a == 0 = b
    | length b == 0 = a
  a <> b = Node
    { _length = length a + length b
    , _children = C.doubleton a b
    }

instance Monoid Encoder where
  mempty = Node 0 mempty

append3 :: Encoder -> Encoder -> Encoder -> Encoder
append3 a b c = Node
  { _length = length a + length b + length c
  , _children = C.tripleton a b c
  }

word8 :: Word8 -> Encoder
word8 = Leaf . Bytes.singleton

singleton :: Bytes -> Encoder
singleton = Leaf

run :: Encoder -> Bytes
run (Leaf bs) = bs
run Node{_length=len0,_children=children0} = runST $ do
  dst <- Prim.newByteArray len0
  let go !ixA eA = case eA of
        Leaf bs -> do
          Bytes.unsafeCopy dst ixA bs
          pure (Bytes.length bs + ixA)
        Node{_length,_children} -> do
          foldlM (\ixB eB -> go ixB eB) ixA _children
  ixC <- foldlM (\ixA e -> go ixA e) 0 children0
  if ixC /= len0
    then errorWithoutStackTrace "Asn.Ber.Encode.run: implementation mistake"
    else do
      dst' <- Prim.unsafeFreezeByteArray dst
      pure Bytes{array=dst',offset=0,length=len0}

encode :: Value -> Bytes
encode = run . encodeValue

encodeValue :: Value -> Encoder
encodeValue v@Value{contents} =
  let theContent = encodeContents contents
   in append3 (valueHeader v) (encodeLength (length theContent)) theContent

valueHeader :: Value -> Encoder
valueHeader Value{tagClass,tagNumber,contents} = byte1 <> extTag
  where
  byte1 = word8 (clsBits .|. pcBits .|. tagBits)
  clsBits = (`unsafeShiftL` 6) $ case tagClass of
    Universal -> 0
    Application -> 1
    ContextSpecific -> 2
    Private -> 3
  pcBits = case contents of
    Constructed _ -> bit 5
    _ -> 0x00
  tagBits = fromIntegral @Word32 @Word8 $ min tagNumber 31
  extTag
    | tagNumber < 31 = mempty
    | otherwise = base128 (fromIntegral @Word32 @Int64 tagNumber) -- FIXME use an unsigned base128 encoder

encodeLength :: Int -> Encoder
encodeLength n
  | n < 128 = word8 $ fromIntegral @Int @Word8 n
  | otherwise =
    let len = base256 (fromIntegral n)
        lenHeader = word8 $ bit 7 .|. (fromIntegral @Int @Word8 (length len))
     in lenHeader <> len

encodeContents :: Contents -> Encoder
encodeContents = \case
  Integer n -> base256 n
  Boolean b -> case b of
    True -> word8 0xFF
    False -> word8 0x00
  UtcTime epochSeconds ->
    let t = Chronos.Time (epochSeconds * 1_000_000_000)
     in case Chronos.timeToDatetime t of
          Chronos.Datetime
            { datetimeDate = Chronos.Date
              { dateYear = Chronos.Year year
              , dateMonth = Chronos.Month month
              , dateDay = Chronos.DayOfMonth day
              }
            , datetimeTime = Chronos.TimeOfDay
              { timeOfDayHour = hour
              , timeOfDayMinute = minute
              , timeOfDayNanoseconds = nanoseconds
              }
            } -> Leaf $ Bytes.fromByteArray $ BB.run Nat.constant $
              encodeTwoDigit (rem year 100)
              `BB.append`
              encodeTwoDigit (month + 1)
              `BB.append`
              encodeTwoDigit day
              `BB.append`
              encodeTwoDigit hour
              `BB.append`
              encodeTwoDigit minute
              `BB.append`
              encodeTwoDigit (fromIntegral @Int64 @Int (quot nanoseconds 1_000_000_000))
              `BB.append`
              BB.ascii 'Z'
  OctetString bs -> bytes bs
  BitString padBits bs -> word8 padBits <> bytes bs
  Null -> mempty
  ObjectIdentifier (Oid arr)
    | Prim.sizeofPrimArray arr < 2 -> error "Object Identifier must have at least two components"
    | otherwise -> objectIdentifier arr
  Utf8String str -> utf8String str
  PrintableString str -> printableString str
  Constructed arr -> constructed arr
  Unresolved raw -> bytes raw

encodeTwoDigit :: Int -> BB.Builder 2
encodeTwoDigit !n =
  BB.word8 (fromIntegral @Int @Word8 (0x30 + quot n 10))
  `BB.append`
  BB.word8 (fromIntegral @Int @Word8 (0x30 + rem n 10))

------------------ Content Encoders ------------------

base128 :: Int64 -> Encoder
base128 = go False (0 :: Int) []
  where
  go !lastNeg !size acc n =
    let content = fromIntegral @Int64 @Word8 (n .&. 0x7F)
        rest = n `unsafeShiftR` 7
        thisNeg = testBit content 6
        atEnd = (content == 0 && rest == 0 && not lastNeg)
              || (content == 0x7F && rest == (-1) && lastNeg)
     in if size /= 0 && atEnd
        then stop acc
        else
          let content' = (if size == 0 then 0 else 0x80) .|. content
           in go thisNeg (size + 1) (content' : acc) rest
  stop acc = singleton . Bytes.fromByteArray . byteArrayFromList $ acc

base256 :: Int64 -> Encoder
base256 n = singleton $ Bytes.fromByteArray (byteArrayFromList minimized)
  where
  byteList =
    [ fromIntegral @Int64 @Word8 $ 0xFF .&. (n `unsafeShiftR` bits)
    | bits <- [56,48..0]
    ]
  minimized
    | n < 0 =
      case dropWhile (==0xFF) byteList of
        bs'@(hd:_) | hd `testBit` 7 -> bs'
        bs' -> 0xFF:bs'
    | otherwise =
      case dropWhile (==0x00) byteList of
        bs'@(hd:_) | not (hd `testBit` 7) -> bs'
        bs' -> 0x00:bs'

bytes :: Bytes -> Encoder
bytes = singleton

objectIdentifier :: PrimArray Word32 -> Encoder
objectIdentifier arr = firstComps <> mconcat restComps
  where
  firstComps = word8 $ fromIntegral @Word32 @Word8 $
    (40 * Prim.indexPrimArray arr 0) + (Prim.indexPrimArray arr 1)
  restComps = [base128 $ fromIntegral @Word32 @Int64 $ Prim.indexPrimArray arr i
              | i <- [2..Prim.sizeofPrimArray arr - 1]]

utf8String :: TS.ShortText -> Encoder
utf8String str = singleton $ shortTextToBytes $ str

printableString :: TS.ShortText -> Encoder
printableString str = singleton $ shortTextToBytes $ str
  -- utf8 is backwards-compatible with ascii, so just hope that the input text is actually printable ascii

constructed :: SmallArray Value -> Encoder
constructed = foldMap' encodeValue

shortTextToBytes :: TS.ShortText -> Bytes
shortTextToBytes str = case TS.toShortByteString str of
  -- ShortText is already utf8-encoded, so just re-wrap it
  SBS arr -> Bytes.fromByteArray (ByteArray arr)