packages feed

dnsbase-1.0.0.0: internal/Net/DNSBase/Internal/Present.hs

-- |
-- Module      : Net.DNSBase.Internal.Present
-- Description : TBD
-- Copyright   : (c) Viktor Dukhovni, 2026
-- License     : BSD-3-Clause
-- Maintainer  : ietf-dane@dukhovni.org
-- Stability   : unstable
module Net.DNSBase.Internal.Present
    ( -- * Presentable class
      Presentable(..)
    -- ** Builder combinators
    , presentByte
    , presentCharSep
    , presentCharSepLn
    , presentLn
    , presentSep
    , presentSepLn
    , presentSp
    , presentSpLn
    -- *** Newtype for parsing and presenting 64-bit epoch times.
    , Epoch64(..)
    -- ** Build directly to a 'String' or 'ByteString'
    , presentString
    , presentStrict
    -- ** Re-exports from "Data.ByteString.Builder"
    , Builder
    , hPutBuilder
    -- *** 'hPutBuilder' specialised to @stdout@
    , putBuilder
    ) where

import qualified Data.ByteString.Builder as B
import qualified Data.ByteString.Builder.Extra as B
import qualified Data.ByteString.Builder.Prim as P
import qualified Data.ByteString.Lazy as L
import qualified Data.ByteString.Lazy.Char8 as L8
import qualified Data.IP.Builder as IP
import qualified System.IO as IO
import Data.ByteString.Builder (hPutBuilder)
import Data.ByteString.Builder.Prim ((>$<), (>*<))
import Data.String (IsString(..))
import Data.Time.Clock.System (SystemTime(..), utcToSystemTime)
import Data.Time.Format (defaultTimeLocale, parseTimeOrError)

import Net.DNSBase.Internal.Util

-- | Return DNS presentation form, as a lazy ByteString builder, taking a
-- continuation.  Since DNS record presentation form is ASCII, we don't need
-- Unicode strings, and lazy ByteString builders perform one to two orders of
-- magnitude faster.
--
-- Complex builders with nested sub-components are much more efficient when
-- constructed in continuation passing style.
--
class Presentable a where
    -- | Serialise the input value with the given continuation.
    present :: a        -- ^ Value to serialise
            -> Builder  -- ^ Continuation
            -> Builder  -- ^ Final output

    -- | Run the builder immediately, producing a lazy 'L.ByteString' with the
    -- given tail.
    presentLazy :: a -- ^ Value to serialise
                -> L.ByteString -- ^ Lazy bytestring suffix
                -> L.ByteString -- ^ Final output
    presentLazy a k = B.toLazyByteStringWith strat k $ present a mempty
      where
        strat = B.safeStrategy 128 B.smallChunkSize

instance Presentable Builder where
    present = (<>)

-- | Append a char, assumed 8-bit only.
instance Presentable Char where
    present = (<>) . B.char8
    {-# INLINE present #-}

-- | Append a string, assumed ASCII.
instance Presentable String where
    present = (<>) . B.string8
    {-# INLINE present #-}

-- | Append a 'ShortByteString' assumed already escaped to not require
-- additional escaping or quoting.
--
instance Presentable ShortByteString where
    present = (<>) . B.shortByteString
    {-# INLINE present #-}

instance Presentable ByteString where
    present = (<>) . B.byteString
    {-# INLINE present #-}

-- Append a decimal Int
instance Presentable Int where
    present = (<>) . B.intDec
    {-# INLINE present #-}

-- Append a decimal Int64
instance Presentable Int64 where
    present = (<>) . B.int64Dec
    {-# INLINE present #-}

-- Append a decimal Int32
instance Presentable Int32 where
    present = (<>) . B.int32Dec
    {-# INLINE present #-}

-- Append a decimal Int16
instance Presentable Int16 where
    present = (<>) . B.int16Dec
    {-# INLINE present #-}

-- Append a decimal Int8
instance Presentable Int8 where
    present = (<>) . B.int8Dec
    {-# INLINE present #-}

-- Append a decimal word8
instance Presentable Word8 where
    present = (<>) . B.word8Dec
    {-# INLINE present #-}

instance Presentable Word16 where
    present = (<>) . B.word16Dec
    {-# INLINE present #-}

instance Presentable Word32 where
    present = (<>) . B.word32Dec
    {-# INLINE present #-}

instance Presentable Word64 where
    present = (<>) . B.word64Dec
    {-# INLINE present #-}

instance Presentable IP where
    present = (<>) . IP.ipBuilder

instance Presentable IPv4 where
    present = (<>) . IP.ipv4Builder

instance Presentable IPv6 where
    present = (<>) . IP.ipv6Builder

-- | Prepend a single literal byte to a continuation builder.  The
-- workhorse separator primitive that the other combinators
-- ('presentSep', 'presentSp', 'presentLn', ...) are built on.
presentByte :: Word8 -> Builder -> Builder
presentByte = (<>) . B.word8
{-# INLINE presentByte #-}

-- | Append the presentation of @a@ followed by a newline (@\\n@,
-- 0x0a).  Use this to terminate each record when emitting a
-- zone-file-style stream, as in
-- @foldr presentLn mempty records@.
presentLn :: Presentable a => a -> Builder -> Builder
presentLn a = present a . presentByte 0x0a
{-# INLINE presentLn #-}

-- | Append with a leading separator
presentSep :: Presentable a => Word8 -> a -> Builder -> Builder
presentSep sep a = presentByte sep . present a
{-# INLINE presentSep #-}

-- | Append with a leading separator and a trailing newline
presentSepLn :: Presentable a => Word8 -> a -> Builder -> Builder
presentSepLn sep a = presentByte sep . presentLn a
{-# INLINE presentSepLn #-}

-- | Append with a leading 'Char' octet separator
presentCharSep :: Presentable a => Char -> a -> Builder -> Builder
presentCharSep sep a = present sep . present a
{-# INLINE presentCharSep #-}

-- | Append with a leading separator and a trailing newline
presentCharSepLn :: Presentable a => Char -> a -> Builder -> Builder
presentCharSepLn sep a = present sep . presentLn a
{-# INLINE presentCharSepLn #-}

-- | Append with a leading space
presentSp :: Presentable a => a -> Builder -> Builder
presentSp = presentSep 0x20
{-# INLINE presentSp #-}

-- | Append with a leading space and a trailing newline
presentSpLn :: Presentable a => a -> Builder -> Builder
presentSpLn a = presentByte 0x20 . presentLn a
{-# INLINE presentSpLn #-}

-- | Immediately construct a strict 'ByteString' from the input followed by
-- the given lazy 'L.ByteString' tail.
presentStrict :: Presentable a => a -> L.ByteString -> ByteString
presentStrict a = L.toStrict . presentLazy a

-- | Immediately construct a 'String' from the input followed by the given
-- tail.
presentString :: Presentable a => a -> String -> String
presentString a k = L8.unpack (presentLazy a mempty) ++ k

-- | Execute the Builder writing output to @IO.stdout@.
-- Typically, @stdout@ should be set in 'IO.BinaryMode' with
-- 'IO.BlockBuffering'.  See 'IO.hSetBinaryMode' and
-- 'IO.hSetBuffering' for details.
--
putBuilder :: Builder -> IO ()
putBuilder = hPutBuilder IO.stdout

-- | 64-bit extended representation of 32-bit DNS clock-arithmetic types.
-- The presentation form is as a YYYYMMDDHHMMSS string.
newtype Epoch64 = Epoch64 Int64
    deriving newtype (Eq, Ord, Enum, Bounded, Num, Real, Integral)

-- | Parse DNSSEC YYYYmmddHHMMSS time format to 'Epoch64' value
instance IsString Epoch64 where
    fromString = coerce . systemSeconds . utcToSystemTime . parseUTC
      where
        parseUTC = parseTimeOrError False defaultTimeLocale "%Y%m%d%H%M%S"

instance Show Epoch64 where
    showsPrec _ e = showChar '"' . presentString e . showChar '"'

instance Presentable Epoch64 where
    -- <http://howardhinnant.github.io/date_algorithms.html>
    -- (years prior 1000 are not supported).
    -- This avoids all the pain of converting epoch time to NominalDiffTime ->
    -- UTCTime -> LocalTime then using formatTime with defaultTimeLocale!
    -- >>> :{
    -- let testVector :: [(String, Epoch64)]
    --     testVector =
    --         [ ( "19230704085602", -1467299038)
    --         , ( "19331017210945", -1142563815)
    --         , ( "19480919012827", -671668293 )
    --         , ( "19631210171455", -191227505 )
    --         , ( "20060819001740", 1155946660 )
    --         , ( "20180723061122", 1532326282 )
    --         , ( "20281019005024", 1855529424 )
    --         , ( "20751108024632", 3340406792 )
    --         , ( "21240926071415", 4883008455 )
    --         , ( "21270331070215", 4962150135 )
    --         , ( "21371220015305", 5300560385 )
    --         , ( "21680118121052", 6249787852 )
    --         , ( "21811012210032", 6683202032 )
    --         , ( "22060719093224", 7464648744 )
    --         , ( "22100427121648", 7583717808 )
    --         , ( "22530821173957", 8950757997 )
    --         , ( "23010804210243", 10463979763)
    --         , ( "23441111161706", 11829514626)
    --         , ( "23750511175551", 12791843751)
    --         , ( "23860427060801", 13137746881) ]
    --  in (==) <$> map (flip presentString "" . snd) <*> map fst $ testVector
    -- :}
    -- True
    --
    present (Epoch64 t) k =
        B.int64Dec year
        <> pad2 mon
        <> pad2 day
        <> pad2 hh
        <> pad2 mm
        <> pad2 ss
        <> k
      where
        (!z0, !s) = t `divMod` 86400
        !z = z0 + 719468
        (!era, !doe) = z `divMod` 146097
        !yoe = (doe - doe `quot` 1460 + doe `quot` 36524
                   - doe `quot` 146096) `quot` 365
        !y = yoe + era * 400
        !doy = doe - (365*yoe + yoe `quot` 4 - yoe `quot` 100)
        !mp = (5*doy + 2) `quot` 153
        !day = doy - (153*mp + 2) `quot` 5 + 1
        !mon = 1 + (mp + 2) `rem` 12
        !year = y + (12 - mon) `quot` 10
        (!hh, (!mm, !ss)) = flip divMod 60 <$> s `divMod` 3600

        pad2 :: Integral a => a -> Builder
        pad2 = P.primBounded w2 . fromIntegral
          where
            w2 = P.condB
                   do (> 9)
                   do P.word8Dec
                   do ((), ) >$< (const 0x30 >$< P.liftFixedToBounded P.word8) >*< P.word8Dec