packages feed

moonlight-triangulation-1.2.0.0: src-serialize/Moonlight/Triangulation/Serialization.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- | The versioned binary surface: encode a triangulation to bytes and read it
-- back. Decoding refuses a payload whose format version or coordinate encoding this
-- build does not own, rather than reinterpreting it.
module Moonlight.Triangulation.Serialization
  ( DecodingBudget (..)
  , TrustedPayloadDecoders
  , trustedBinaryPayloadDecoders
  , SerializedCountKind (..)
  , SerializationError (..)
  , serializationVersion
  , encodeTriangulation
  , decodeTriangulation
  ) where

import Control.Monad (unless, when)
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Except (ExceptT, runExceptT, throwE)
import Data.Binary (Binary (..))
import Data.Binary.Get
  ( Get
  , bytesRead
  , getDoublebe
  , getWord16be
  , getWord32be
  , getWord64be
  , getWord8
  , runGetOrFail
  )
import Data.Binary.Put
  ( putDoublebe
  , putWord16be
  , putWord32be
  , putWord64be
  , putWord8
  , runPut
  )
import qualified Data.ByteString.Lazy as BL
import Data.Foldable (traverse_)
import qualified Data.IntSet as IntSet
import Data.Int (Int64)
import Data.List.NonEmpty (NonEmpty (..))
import Data.Proxy (Proxy (..))
import qualified Data.Set as Set
import qualified Data.Vector as V
import qualified Data.Vector.Unboxed as U
import Data.Word (Word16, Word64, Word8)
import Moonlight.Triangulation.Internal.BoxedPaged
  ( boxedFill
  , boxedFromVector
  , boxedToVector
  )
import Moonlight.Triangulation.Internal.Paged (fromLocalVector, fromVector, toVector)
import Moonlight.Triangulation.Internal.PackedIndex (indexLimit)
import Moonlight.Triangulation.Internal.PointIndex (buildPointIndex)
import Moonlight.Triangulation.Handles.HandleDefs
import Moonlight.Triangulation.Internal.Representation
import Moonlight.Triangulation.Internal.Types
import Moonlight.Triangulation.Math (mkQueryPoint)
import Moonlight.Triangulation.Validation (validateTriangulation)

instance Binary (Point) where
  put (Point x y) = putDoublebe x >> putDoublebe y
  get = Point <$> getDoublebe <*> getDoublebe

instance Binary VertexId where
  put (VertexId value) = putWord32be value
  get = VertexId <$> getWord32be

instance Binary FaceId where
  put (FaceId value) = putWord32be value
  get = FaceId <$> getWord32be

instance Binary DirectedEdgeId where
  put (DirectedEdgeId value) = putWord32be value
  get = DirectedEdgeId <$> getWord32be

instance Binary UndirectedEdgeId where
  put (UndirectedEdgeId value) = putWord32be value
  get = UndirectedEdgeId <$> getWord32be

-- | The finite resource envelope admitted by the canonical decoder. The byte
-- budget bounds the complete input before parsing; the element budget bounds
-- the total number of library-owned serialized section elements before any
-- section is allocated.
data DecodingBudget = DecodingBudget
  { decodingMaximumInputBytes :: !Word64
  , decodingMaximumSectionElements :: !Word64
  }
  deriving stock (Eq, Show)

-- | Evidence that the caller accepts the internal resource behavior of all
-- four payload decoders. The structural budget governs only containers owned
-- by this module; executable payload decoders require this separate trust law.
data TrustedPayloadDecoders vertex directed undirected face where
  TrustedBinaryPayloadDecoders
    :: ( Binary vertex
       , Binary directed
       , Binary undirected
       , Binary face
       )
    => TrustedPayloadDecoders vertex directed undirected face

-- | Explicitly trust the selected 'Binary' payload instances. This witness is
-- required because arbitrary instances may allocate independently of input
-- bytes; constructing it declares that the caller has audited that behavior.
trustedBinaryPayloadDecoders
  :: ( Binary vertex
     , Binary directed
     , Binary undirected
     , Binary face
     )
  => TrustedPayloadDecoders vertex directed undirected face
trustedBinaryPayloadDecoders = TrustedBinaryPayloadDecoders

-- | The structural count whose encoded value was outside the resident index
-- or allocation domain.
data SerializedCountKind
  = SerializedVertexCount
  | SerializedDirectedEdgeCount
  | SerializedFaceCount
  | SerializedConstraintCount
  deriving stock (Eq, Ord, Show)

-- | Every way serialization refuses, each naming its witness.
data SerializationError
  = BinaryDecodeFailure !Int64 !String
  | TrailingBytes !Int64
  | InvalidFormatMagic !Word64
  | UnsupportedFormatVersion !Word16
  | ConstraintModeTagMismatch !Word8 !Word8
  | CoordinateEncodingTagMismatch !Word8 !Word8
  | InputByteBudgetExceeded !Word64 !Word64
  | DecodedSectionBudgetExceeded !Word64 !Word64
  | EncodedCountExceedsInt !SerializedCountKind !Word64
  | EncodedCountExceedsPackedIndex !SerializedCountKind !Word64 !Word64
  | SerializedDirectedEdgeCountOdd !Word64
  | SerializedConstraintCountExceedsEdges !Word64 !Word64
  | SerializedPlanarCardinalityMismatch !Word64 !Word64 !Word64
  | SerializedFixedBodyTooShort !Word64 !Word64
  | SerializedMissingOuterFace
  | InvalidSerializedPoint {-# UNPACK #-} !Int !PointValidationError
  | NonCanonicalSerializedConstraintFlag !UndirectedEdgeId !Word8
  | SerializedConstraintCountMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
  | UnconstrainedSerializedConstraints {-# UNPACK #-} !Int
  | DuplicateSerializedCoordinates {-# UNPACK #-} !Int {-# UNPACK #-} !Int
  | DecodedInvariantViolations !(NonEmpty InvariantViolation)
  deriving stock (Eq, Show)

type Decoder = ExceptT SerializationError Get

-- | The envelope version this module writes.
serializationVersion :: Word16
serializationVersion = 6

formatMagic :: Word64
formatMagic = 0x5350414445485307 -- "SPADEHS" + canonical geometry-owned format family

binary64EncodingTag :: Word8
binary64EncodingTag = 2

-- | Write the versioned binary envelope.
encodeTriangulation
  :: forall mode vertex directed undirected face. (KnownConstraintMode mode, Binary vertex, Binary directed, Binary undirected, Binary face)
  => Triangulation mode vertex directed undirected face
  -> BL.ByteString
encodeTriangulation triangulation = runPut $ do
  putWord64be formatMagic
  putWord16be serializationVersion
  putWord8 (modeTag (constraintModeValue (modeProxy triangulation)))
  putWord8 binary64EncodingTag
  let ElementDefaults directedDefault undirectedDefault faceDefault = triElementDefaults triangulation
      pointXs = toVector (triPointX triangulation)
      pointYs = toVector (triPointY triangulation)
      vertexDefault = boxedFill (triVertexData triangulation)
      vertexDataVector = boxedToVector (triVertexData triangulation)
      vertexOut = toVector (triVertexOut triangulation)
      topology = toVector (triHalfTopology triangulation)
      directedDataVector = boxedToVector (triDirectedData triangulation)
      undirectedDataVector = boxedToVector (triUndirectedData triangulation)
      faceEdge = toVector (triFaceEdge triangulation)
      faceDataVector = boxedToVector (triFaceData triangulation)
      constraints = toVector (triConstraint triangulation)
      vertexCount = U.length pointXs
      directedEdgeCount = U.length topology `quot` 4
      faceCount = U.length faceEdge
  -- Version 6 commits every structural count in one prefix. The decoder can
  -- prove their relationships and resource bounds before defaults, payloads,
  -- or section bodies are evaluated.
  putWord64be (fromIntegral vertexCount)
  putWord64be (fromIntegral directedEdgeCount)
  putWord64be (fromIntegral faceCount)
  putWord64be (fromIntegral (triConstraintCount triangulation))
  put directedDefault
  put undirectedDefault
  put faceDefault
  -- Geometry and payloads are independent components. Persist the authoritative
  -- coordinate pages rather than attempting to recover them from annotations.
  U.mapM_ putDoublebe pointXs
  U.mapM_ putDoublebe pointYs
  put vertexDefault
  V.mapM_ put vertexDataVector
  U.mapM_ putWord32be vertexOut
  -- The wire format stores the four topology planes separately; the interleaved
  -- arena is a resident layout, not a serialization concern.
  let plane field = U.generate directedEdgeCount (\edge -> topology U.! (4 * edge + field))
  U.mapM_ putWord32be (plane 0)
  U.mapM_ putWord32be (plane 1)
  U.mapM_ putWord32be (plane 2)
  U.mapM_ putWord32be (plane 3)
  V.mapM_ put directedDataVector
  V.mapM_ put undirectedDataVector
  U.mapM_ putWord32be faceEdge
  V.mapM_ put faceDataVector
  U.mapM_ putWord8 constraints

-- | Decode one exact, versioned finite DCEL inside an explicit resource
-- envelope. Structural counts are read and reconciled before any default,
-- payload, or section body is decoded. Coordinate uniqueness and the complete
-- topology, geometry, and Delaunay/CDT invariants are checked before the opaque
-- value is returned.
--
-- The budget bounds the input and the containers owned by this module. The
-- required 'TrustedPayloadDecoders' witness separately records the caller's
-- decision that every selected payload decoder is internally resource-safe.
decodeTriangulation
  :: forall mode vertex directed undirected face.
     KnownConstraintMode mode
  => DecodingBudget
  -> TrustedPayloadDecoders vertex directed undirected face
  -> BL.ByteString
  -> Either SerializationError (Triangulation mode vertex directed undirected face)
decodeTriangulation budget TrustedBinaryPayloadDecoders bytes
  | inputByteCount > decodingMaximumInputBytes budget =
      Left
        ( InputByteBudgetExceeded
            inputByteCount
            (decodingMaximumInputBytes budget)
        )
  | otherwise =
      case runGetOrFail (runExceptT getTriangulation) bytes of
        Left (_, offset, message) -> Left (BinaryDecodeFailure offset message)
        Right (_, _, Left failure) -> Left failure
        Right (trailing, _, Right triangulation)
          | not (BL.null trailing) -> Left (TrailingBytes (BL.length trailing))
          | otherwise ->
              case validateTriangulation triangulation of
                [] -> Right triangulation
                firstViolation : remainingViolations ->
                  Left (DecodedInvariantViolations (firstViolation :| remainingViolations))
 where
  inputByteCount = fromIntegral (BL.length bytes)

  getTriangulation :: Decoder (Triangulation mode vertex directed undirected face)
  getTriangulation = do
    magic <- lift getWord64be
    unless (magic == formatMagic) (throwE (InvalidFormatMagic magic))
    version <- lift getWord16be
    unless (version == serializationVersion) (throwE (UnsupportedFormatVersion version))
    encodedMode <- lift getWord8
    let expectedMode = modeTag (constraintModeValue (Proxy :: Proxy mode))
    unless (encodedMode == expectedMode) (throwE (ConstraintModeTagMismatch expectedMode encodedMode))
    encodedScalar <- lift getWord8
    let expectedScalar = binary64EncodingTag
    unless (encodedScalar == expectedScalar) (throwE (CoordinateEncodingTagMismatch expectedScalar encodedScalar))
    encodedVertexCount <- lift getWord64be
    encodedDirectedEdgeCount <- lift getWord64be
    encodedFaceCount <- lift getWord64be
    encodedConstraintCount <- lift getWord64be
    validateStructuralPrefix
      budget
      encodedVertexCount
      encodedDirectedEdgeCount
      encodedFaceCount
      encodedConstraintCount
    prefixByteCount <- fromIntegral <$> lift bytesRead
    let bodyByteCount = inputByteCount - prefixByteCount
        minimumBodyByteCount =
          minimumFixedBodyBytes
            encodedVertexCount
            encodedDirectedEdgeCount
            encodedFaceCount
    unless (bodyByteCount >= minimumBodyByteCount) $
      throwE (SerializedFixedBodyTooShort bodyByteCount minimumBodyByteCount)

    let vertexCount = fromIntegral encodedVertexCount
        halfCount = fromIntegral encodedDirectedEdgeCount
        edgeCount = halfCount `quot` 2
        faceCount = fromIntegral encodedFaceCount
        cachedConstraintCount = fromIntegral encodedConstraintCount
    defaults <- ElementDefaults <$> lift get <*> lift get <*> lift get
    pointXs <- U.replicateM vertexCount (lift getDoublebe)
    pointYs <- U.replicateM vertexCount (lift getDoublebe)
    vertexDefault <- lift get
    vertexDataVector <- V.replicateM vertexCount (lift get)
    vertexOut <- U.replicateM vertexCount (lift getWord32be)
    halfOrigin <- U.replicateM halfCount (lift getWord32be)
    halfNext <- U.replicateM halfCount (lift getWord32be)
    halfPrev <- U.replicateM halfCount (lift getWord32be)
    halfFace <- U.replicateM halfCount (lift getWord32be)
    directedDataVector <- V.replicateM halfCount (lift get)
    undirectedDataVector <- V.replicateM edgeCount (lift get)
    faceEdge <- U.replicateM faceCount (lift getWord32be)
    faceDataVector <- V.replicateM faceCount (lift get)
    constraints <- U.replicateM edgeCount (lift getWord8)

    let points = V.generate vertexCount (\index -> Point (pointXs U.! index) (pointYs U.! index))
    traverse_ (uncurry validateStoredPoint) (V.indexed points)

    case U.ifoldr (\index flag found -> if flag /= 0 && flag /= 1 then Just (index, flag) else found) Nothing constraints of
      Nothing -> pure ()
      Just (index, flag) ->
        throwE
          ( NonCanonicalSerializedConstraintFlag
              (UndirectedEdgeId (fromIntegral index))
              flag
          )
    let actualConstraintCount = U.foldl' (\count flag -> if flag == 1 then count + 1 else count) 0 constraints
    unless (cachedConstraintCount == actualConstraintCount) (throwE (SerializedConstraintCountMismatch cachedConstraintCount actualConstraintCount))
    when (expectedMode == 0 && actualConstraintCount /= 0) (throwE (UnconstrainedSerializedConstraints actualConstraintCount))

    let distinctPointCount = Set.size (V.foldl' (flip Set.insert) Set.empty points)
    unless (distinctPointCount == vertexCount) (throwE (DuplicateSerializedCoordinates vertexCount distinctPointCount))

    let pointXStore = fromLocalVector 0 pointXs
        pointYStore = fromLocalVector 0 pointYs
        topologyStore =
          fromVector maxBound $
            U.generate (4 * halfCount) $ \slot ->
              let (edge, field) = slot `quotRem` 4
               in case field of
                    0 -> halfOrigin U.! edge
                    1 -> halfNext U.! edge
                    2 -> halfPrev U.! edge
                    _ -> halfFace U.! edge
        constraintEdgeIndex =
          U.ifoldl'
            (\edges index flag ->
               if flag == 1 then IntSet.insert index edges else edges
            )
            IntSet.empty
            constraints
    pure
      Triangulation
        { triPointX = pointXStore
        , triPointY = pointYStore
        , triPointIndex = buildPointIndex pointXStore pointYStore
        , triVertexOut = fromLocalVector maxBound vertexOut
        , triVertexData = boxedFromVector vertexDefault vertexDataVector
        , triHalfTopology = topologyStore
        , triDirectedData = boxedFromVector (Just (defaultDirectedEdgeData defaults)) directedDataVector
        , triUndirectedData = boxedFromVector (Just (defaultUndirectedEdgeData defaults)) undirectedDataVector
        , triFaceEdge = fromLocalVector maxBound faceEdge
        , triFaceData = boxedFromVector (Just (defaultFaceData defaults)) faceDataVector
        , triConstraint = fromVector 0 constraints
        , triConstraintCount = cachedConstraintCount
        , triConstraintEdges = constraintEdgeIndex
        , triElementDefaults = defaults
        }

validateStoredPoint :: Int -> Point -> Decoder ()
validateStoredPoint index point =
  case mkQueryPoint point of
    Left failure -> throwE (InvalidSerializedPoint index failure)
    Right _ -> pure ()

modeProxy :: Triangulation mode vertex directed undirected face -> Proxy mode
modeProxy _ = Proxy

modeTag :: ConstraintMode -> Word8
modeTag Unconstrained = 0
modeTag Constrained = 1

validateStructuralPrefix
  :: DecodingBudget
  -> Word64
  -> Word64
  -> Word64
  -> Word64
  -> Decoder ()
validateStructuralPrefix budget vertexCount directedEdgeCount faceCount constraintCount = do
  traverse_
    (uncurry validateEncodedCount)
    [ (SerializedVertexCount, vertexCount)
    , (SerializedDirectedEdgeCount, directedEdgeCount)
    , (SerializedFaceCount, faceCount)
    , (SerializedConstraintCount, constraintCount)
    ]
  unless (even directedEdgeCount) $
    throwE (SerializedDirectedEdgeCountOdd directedEdgeCount)
  unless (faceCount >= 1) (throwE SerializedMissingOuterFace)
  let undirectedEdgeCount = directedEdgeCount `quot` 2
  unless (constraintCount <= undirectedEdgeCount) $
    throwE
      ( SerializedConstraintCountExceedsEdges
          constraintCount
          undirectedEdgeCount
      )
  unless
    (planarCardinalityHolds vertexCount directedEdgeCount faceCount)
    ( throwE
        ( SerializedPlanarCardinalityMismatch
            vertexCount
            directedEdgeCount
            faceCount
        )
    )
  let sectionElements =
        4 * vertexCount
          + 6 * directedEdgeCount
          + 2 * faceCount
      maximumElements = decodingMaximumSectionElements budget
  when (sectionElements > maximumElements) $
    throwE (DecodedSectionBudgetExceeded sectionElements maximumElements)

validateEncodedCount :: SerializedCountKind -> Word64 -> Decoder ()
validateEncodedCount kind count = do
  when (count > fromIntegral (maxBound :: Int)) $
    throwE (EncodedCountExceedsInt kind count)
  when (count > maximumPackedElementCount) $
    throwE
      ( EncodedCountExceedsPackedIndex
          kind
          count
          maximumPackedElementCount
      )

-- Each resident handle is a Word32 with @maxBound@ withheld as the optional
-- no-index marker. A count may include every remaining representable handle.
maximumPackedElementCount :: Word64
maximumPackedElementCount = fromIntegral indexLimit + 1

-- Every resident triangulation is a connected planar straight-line graph.
-- Empty input has only the outer face; a bounded-face-free nonempty graph is the
-- collinear chain; otherwise Euler's law determines the edge count. All
-- arithmetic is safe after 'validateEncodedCount' bounds each term to the
-- packed-index domain.
planarCardinalityHolds :: Word64 -> Word64 -> Word64 -> Bool
planarCardinalityHolds vertexCount directedEdgeCount faceCount
  | vertexCount == 0 = directedEdgeCount == 0 && faceCount == 1
  | faceCount == 1 = directedEdgeCount == 2 * (vertexCount - 1)
  | vertexCount < 3 = False
  | otherwise =
      directedEdgeCount == 2 * (vertexCount + faceCount - 2)

-- The packed-index proof above bounds every term far below Word64 overflow.
-- Payloads and defaults have no format-level lower bound because lawful
-- 'Binary' decoders such as that for @()@ may consume zero bytes.
minimumFixedBodyBytes :: Word64 -> Word64 -> Word64 -> Word64
minimumFixedBodyBytes vertexCount directedEdgeCount faceCount =
  20 * vertexCount
    + 16 * directedEdgeCount
    + 4 * faceCount
    + directedEdgeCount `quot` 2