isobmff-builder-0.1.0.1: src/Data/ByteString/IsoBaseFileFormat/Boxes/Box.hs
{-# LANGUAGE UndecidableInstances #-}
-- | Definition of the most basic element in an ISOBMFF file: a /box/. See
-- Chapter 4 in the standard document. A box is a container with a type, a
-- size, possible some data and some nested boxes. The standard defines - among
-- other characteristics - what boxes exist, what data they contain and how they
-- are nested into each other. This library tries to capture some of these
-- characteristics using modern Haskell type system features to provide compile
-- checks for (partial) standard compliance.
module Data.ByteString.IsoBaseFileFormat.Boxes.Box
(module Data.ByteString.IsoBaseFileFormat.Boxes.Box, module X)
where
import Data.Bits as X
import Data.ByteString.Builder as X
import Data.Monoid as X
import Data.Proxy as X
import Data.Word as X
import GHC.TypeLits as X
import Data.String
import Data.Type.Equality
import Data.Type.List
import Data.Type.Bool
import Data.Type.Equality
import GHC.Exts
-- * Basic Types and classes
-- | A class that describes (on the type level) how a box can be nested into
-- other boxes (see 'Boxes').
class BoxRules (t :: k) where
-- | List of boxes that this box can be nested into.
type RestrictedTo t :: Maybe [k]
type RestrictedTo t = 'Just '[]
-- | If the box is also allowed 'top-level' i.e. in the file directly, not
-- nested in an other box.
type IsTopLevelBox t :: Bool
type IsTopLevelBox t = 'True
-- | Describes which nested boxes MUST be present in a box using 'boxes'.
type RequiredNestedBoxes t :: [k]
type RequiredNestedBoxes t = '[]
-- | Describes how many times a box should be present in a container (-box).
type GetCardinality t (c :: k) :: Cardinality
type GetCardinality t any = ExactlyOnce
-- | Describes how many times a box should be present in a container.
data Cardinality = AtMostOnce | ExactlyOnce | OnceOrMore
-- | Convert type level box types to values
class BoxRules t => IsBoxType (t :: k) where
toBoxType :: proxy t -> BoxType
instance (BoxRules t, KnownSymbol t) => IsBoxType t where
toBoxType _ = StdType (fromString (symbolVal (Proxy :: Proxy t)))
-- | Types that go into a box. A box content is a piece of data that can be
-- reused in different instances of 'IsBox'. It has no 'BoxType' and hence
-- defines no box.
class IsBoxContent a where
boxSize :: a -> BoxSize
boxBuilder :: a -> Builder
-- | An empty box content can by represented by @()@ (i.e. /unit/).
instance IsBoxContent () where
boxSize _ = 0
boxBuilder _ = mempty
-- | Box content composed of box contents @a@ and @b@.
data Extend a b =
Extend a
b
instance (IsBoxContent p,IsBoxContent c) => IsBoxContent (Extend p c) where
boxSize (Extend p c) = boxSize p + boxSize c
boxBuilder (Extend p c) = boxBuilder p <> boxBuilder c
-- * Boxes
-- | Create a 'Box' with a 'StdType' 'FourCc' type.
box :: forall t c.
(IsBoxType t,IsBoxContent c)
=> c -> Box t
box cnt = Box (toBoxType (Proxy :: Proxy t)) cnt
-- | An /empty/ box. This is for boxes without fields. All these boxes contain
-- is their obligatory 'BoxHeader' possibly nested boxes.
emptyBox :: forall t . (IsBoxType t) => Box t
emptyBox = box ()
-- | A type that wraps the contents of a box and the box type.
data Box (b :: t) where
Box :: (IsBoxType t,IsBoxContent c) => BoxType -> c -> Box t
instance IsBoxContent (Box t) where
boxBuilder b@(Box t cnt) = sFix <> tFix <> sExt <> tExt <> boxBuilder cnt
where s = boxSize b
sFix = boxBuilder s
sExt = boxBuilder (BoxSizeExtension s)
tFix = boxBuilder t
tExt = boxBuilder (BoxTypeExtension t)
boxSize b@(Box t cnt) = sPayload + boxSize (BoxSizeExtension sPayload)
where sPayload =
boxSize sPayload + boxSize t + boxSize cnt +
boxSize (BoxTypeExtension t)
-- * Box Size and Type
-- | The size of the box. If the size is limited to a (fixed) value, it can be
-- provided as a 'Word64' which will be represented as either a 32bit compact
-- size or as 64 bit /largesize/. If 'UnlimitedSize' is used, the box extends to
-- the end of the file.
data BoxSize
= UnlimitedSize
| BoxSize Word64
deriving (Show,Eq)
instance IsBoxContent BoxSize where
boxSize _ = BoxSize 4
boxBuilder UnlimitedSize = word32BE 0
boxBuilder (BoxSize n) =
word32BE $
if n < 2 ^ 32
then fromIntegral n
else 1
instance Num BoxSize where
(+) UnlimitedSize _ = UnlimitedSize
(+) _ UnlimitedSize = UnlimitedSize
(+) (BoxSize l) (BoxSize r) = BoxSize (l + r)
(-) UnlimitedSize _ = UnlimitedSize
(-) _ UnlimitedSize = UnlimitedSize
(-) (BoxSize l) (BoxSize r) = BoxSize (l - r)
(*) UnlimitedSize _ = UnlimitedSize
(*) _ UnlimitedSize = UnlimitedSize
(*) (BoxSize l) (BoxSize r) = BoxSize (l * r)
abs UnlimitedSize = UnlimitedSize
abs (BoxSize n) = BoxSize (abs n)
signum UnlimitedSize = UnlimitedSize
signum (BoxSize n) = BoxSize (signum n)
fromInteger n = BoxSize $ fromInteger n
-- | The 'BoxSize' can be > 2^32 in which case an 'BoxSizeExtension' must be
-- added after the type field.
data BoxSizeExtension =
BoxSizeExtension BoxSize
instance IsBoxContent BoxSizeExtension where
boxBuilder (BoxSizeExtension UnlimitedSize) = mempty
boxBuilder (BoxSizeExtension (BoxSize n)) =
if n < 2 ^ 32
then mempty
else word64BE n
boxSize (BoxSizeExtension UnlimitedSize) = 0
boxSize (BoxSizeExtension (BoxSize n)) =
BoxSize $
if n < 2 ^ 32
then 0
else 8
-- | A box has a /type/, this is the value level representation for the box type.
data BoxType
=
-- | `FourCc` can be used as @boxType@ in `Box`, standard four letter character
-- code, e.g. @ftyp@
StdType FourCc
|
-- | CustomBoxType defines custom @boxType@s in `Box`es.
CustomBoxType String
deriving (Show,Eq)
-- | A type containin a printable four letter character code.
newtype FourCc =
FourCc (Char,Char,Char,Char)
deriving (Show,Eq)
instance IsString FourCc where
fromString str
| length str == 4 =
let [a,b,c,d] = str
in FourCc (a,b,c,d)
| otherwise =
error ("cannot make a 'FourCc' of a String which isn't exactly 4 bytes long: " ++
show str ++ " has a length of " ++ show (length str))
instance IsBoxContent FourCc where
boxSize _ = 4
boxBuilder (FourCc (a,b,c,d)) = putW a <> putW b <> putW c <> putW d
where putW = word8 . fromIntegral . fromEnum
instance IsBoxContent BoxType where
boxSize _ = boxSize (FourCc undefined)
boxBuilder t =
case t of
StdType x -> boxBuilder x
CustomBoxType u -> boxBuilder (FourCc ('u','u','i','d'))
-- | When using custom types extra data must be written after the extra size
-- information. Since the box type and the optional custom box type are not
-- guaranteed to be consequtive, this type handles the /second/ part seperately.
data BoxTypeExtension =
BoxTypeExtension BoxType
instance IsBoxContent BoxTypeExtension where
boxSize (BoxTypeExtension (StdType _)) = 0
boxSize (BoxTypeExtension (CustomBoxType _)) = 16 * 4
boxBuilder (BoxTypeExtension (StdType _)) = mempty
boxBuilder (BoxTypeExtension (CustomBoxType str)) =
mconcat (map (word8 . fromIntegral . fromEnum)
(take (16 * 4) str) ++
repeat (word8 0))
-- * Type-safe box composition
-- | A box that may contain nested boxes. The nested boxes are type checked to
-- be valid in the container box. This results in a container-box with only
-- valid and all required child boxes. This is checked by the type system.
boxes :: forall ts t.
(IsBoxType t,ValidBoxes t ts)
=> Boxes t ts -> Box t
boxes = box
-- | A container-box with child boxes.
data Boxes (cont :: x) (boxTypes :: [x]) where
Parent :: IsBoxType t => Box t -> Boxes t '[]
(:-) :: IsBoxType t => Boxes c ts -> Box t -> Boxes c (t ': ts)
infixl 2 :-
-- | A container box that contains only the parent, and no children (yet).
type Container parent = Boxes parent '[]
-- | An operator for @Parent parent :- firstChild@
(^-) :: (IsBoxType t, IsBoxType u) => Box t -> Box u -> Boxes t '[u]
parent ^- firstChild = Parent parent :- firstChild
infixl 2 ^-
-- | To be nested into a box, 'Boxes' must be an instance of 'IsBoxContent'.
-- This instance concatenates all nested boxes.
instance (IsBoxType t,ValidBoxes t bs) => IsBoxContent (Boxes t bs) where
boxSize bs = boxSize (UnverifiedBoxes bs)
boxBuilder bs = boxBuilder (UnverifiedBoxes bs)
-- | An internal wrapper type around 'Boxes' for the 'IsBoxContent' instance.
-- Since the 'IsBoxContent' instance recursivly deconstructs a 'Boxes' the
-- constraints for the validity 'ValidBoxes' cannot be asserted. To circumvent
-- this the 'IsBoxContent' instance for 'Boxes' delegates to the instance of
-- 'UnverifiedBoxes', which has no 'ValidBoxes' constraint in the instance head.
newtype UnverifiedBoxes t ts = UnverifiedBoxes (Boxes t ts)
instance IsBoxContent (UnverifiedBoxes t bs) where
boxSize (UnverifiedBoxes (Parent c)) = boxSize c
boxSize (UnverifiedBoxes (bs :- b)) = boxSize (UnverifiedBoxes bs) + boxSize b
boxBuilder (UnverifiedBoxes (Parent c)) = boxBuilder c
boxBuilder (UnverifiedBoxes (bs :- b)) = boxBuilder (UnverifiedBoxes bs) <> boxBuilder b
-- * Type level consistency checks
-- | A type-level check that uses 'BoxRules' to check that the contained boxes
-- are standard conform.
type ValidBoxes t ts =
( AllAllowedIn t ts ~ 'True
, HasAllRequiredBoxes t (RequiredNestedBoxes t) ts ~ 'True
, CheckTopLevelOk t ~ 'True)
-- | A type function to check that all nested boxes are allowed in the
-- container.
type family AllAllowedIn (container :: k) (boxes :: [k]) :: Bool
where
AllAllowedIn c '[] = 'True
AllAllowedIn c (t ': ts) =
If (CheckAllowedIn c t (RestrictedTo t))
(AllAllowedIn c ts)
(TypeError (NotAllowedMsg c t))
type family CheckAllowedIn (c :: k) (t :: k) (a :: Maybe [k]) :: Bool where
CheckAllowedIn c t 'Nothing = 'True
CheckAllowedIn c t ('Just rs) = Find c rs
-- | The custom (type-) error message for 'AllAllowedIn'.
type NotAllowedMsg c t =
Text "Boxes of type: "
:<>: ShowType c
:<>: Text " may not contain boxes of type "
:<>: ShowType t
:$$: Text "Valid containers for "
:<>: ShowType t
:<>: Text " boxes are: "
:$$: ShowType (RestrictedTo t)
:$$: ShowType t
:<>: If (IsTopLevelBox c)
(Text " boxes may appear top-level in a file.")
(Text " boxes must be nested.")
-- | Check that all required boxes have been nested.
type family HasAllRequiredBoxes (c :: k) (req :: [k]) (nested :: [k]) :: Bool
where
HasAllRequiredBoxes c '[] nested = 'True
HasAllRequiredBoxes c (r ': restReq) nested =
If (Find r nested)
(HasAllRequiredBoxes c restReq nested)
(TypeError (MissingRequired c r nested))
type IsSubSet base sub = Intersection base sub == sub
-- | The custom (type-) error message for 'HasAllRequiredBoxes'.
type MissingRequired c r nested =
Text "Boxes of type: "
:<>: ShowType c
:<>: Text " require these nested boxes: "
:<>: ShowType (RequiredNestedBoxes c)
:$$: Text "but only these box types were nested: "
:<>: ShowType nested
:$$: Text "e.g. this type is missing: "
:<>: ShowType r
-- | Check that the box may appear top-level.
type family CheckTopLevelOk (t :: k) :: Bool where
CheckTopLevelOk t = IsTopLevelBox t || TypeError (NotTopLevenError t)
-- | The custom (type-) error message indicating that a box may not appear
-- top-level.
type NotTopLevenError c =
Text "Boxes of type "
:<>: ShowType c
:<>: Text " MUST be nested inside boxes of these types: "
:$$: ShowType (RestrictedTo c)
-- * Full Boxes
-- | A `Box` with /version/ and /branding/ information
type FullBox t = Extend FullBoxHeader t
-- | Create a 'FullBox' from a 'FourCc' 'StdType' and the nested box content.
fullBox
:: (IsBoxType t, IsBoxContent c)
=> BoxVersion -> BoxFlags 24 -> c -> Box t
fullBox ver fs cnt = box (Extend (FullBoxHeader ver fs) cnt)
-- | The additional header with /version/ and /branding/ information
data FullBoxHeader =
FullBoxHeader BoxVersion
(BoxFlags 24)
instance IsBoxContent FullBoxHeader where
boxSize (FullBoxHeader _ f) = 1 + boxSize f
boxBuilder (FullBoxHeader (BoxVersion v) f) = word8 v <> boxBuilder f
-- | The box version (in a 'FullBox') is a single byte
newtype BoxVersion =
BoxVersion Word8
-- | In addition to a 'BoxVersion' there can be 24 bits for custom flags etc in
-- a 'FullBox'.
newtype BoxFlags bits =
BoxFlags Integer
deriving (Eq,Show,Num)
-- | Internal function that creates a bit mask with all bits in a 'BoxFlags' set
-- to 1.
boxFlagBitMask :: KnownNat bits
=> BoxFlags bits -> Integer
boxFlagBitMask px = 2 ^ natVal px - 1
-- | Internal function that masks-out all bits higher than 'bits'.
cropBits :: KnownNat bits
=> BoxFlags bits -> BoxFlags bits
cropBits f@(BoxFlags b) = BoxFlags (b .&. boxFlagBitMask f)
-- | Get the number of bytes required to store a number of bits.
instance KnownNat bits => IsBoxContent (BoxFlags bits) where
boxSize f =
let minBytes = fromInteger $ natVal f `div` 8
modBytes = fromInteger $ natVal f `mod` 8
in BoxSize $ minBytes + signum modBytes
boxBuilder f@(BoxFlags b) =
let bytes =
let (BoxSize bytes') = boxSize f
in fromIntegral bytes'
wordSeq n
| n <= bytes =
word8 (fromIntegral (shiftR b ((bytes - n) * 8) .&. 255)) <>
wordSeq (n + 1)
| otherwise = mempty
in wordSeq 1
instance KnownNat bits => Bits (BoxFlags bits) where
(.&.) lf@(BoxFlags l) (BoxFlags r) = cropBits $ BoxFlags $ l .&. r
(.|.) lf@(BoxFlags l) (BoxFlags r) = cropBits $ BoxFlags $ l .&. r
xor (BoxFlags l) (BoxFlags r) = cropBits $ BoxFlags $ xor l r
complement (BoxFlags x) = cropBits $ BoxFlags $ complement x
shift (BoxFlags x) = cropBits . BoxFlags . shift x
rotateL = error "TODO rotateL"
rotateR = error "TODO rotateR"
bitSize = fromInteger . natVal
bitSizeMaybe = Just . fromInteger . natVal
isSigned _ = False
testBit f n =
let (BoxFlags b) = cropBits f
in testBit b n
bit = cropBits . BoxFlags . bit
popCount f =
let (BoxFlags b) = cropBits f
in popCount b
zeroBits = BoxFlags 0