packages feed

named-binary-tag (empty) → 0.1.0.0

raw patch · 6 files changed

+436/−0 lines, 6 filesdep +basedep +bytestringdep +cereal

Dependencies added: base, bytestring, cereal, containers, indexed-traversable, named-binary-tag, rrb-vector, tasty, tasty-quickcheck, text, zlib

Files

+ CHANGELOG.md view
@@ -0,0 +1,7 @@+# Revision history for named-binary-tag++## 0.1.0.0 -- 2022-07-03++First version.++
+ LICENSE view
@@ -0,0 +1,21 @@+MIT License++Copyright (c) 2022 David Garland++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ README.md view
@@ -0,0 +1,34 @@+# named-binary-tag++A replacement for the old [nbt](https://github.com/acfoltzer/nbt) package, because that one has+been defunct for 2 years due to missing a single-line fix.++There are also a few improvements(?) made while I was at it:+- You can choose between a version with or without label maps using `MapNbt` or `Nbt'` respectively.+- Uses konsumlamm's [rrb-vector](https://github.com/konsumlamm/rrb-vector) package instead of a mix of lists and unboxed arrays for more well-rounded asymptotics.+- `StrictData` is enabled.+- Names of types and constructors are shortened a bit, with the reasoning that you should really be using qualified access for most of them.+- The code is shorter and better documented now (not that it really needed any documentation...)+- Less boilerplate is required to use it, at the expense of having `zlib` as a dependency. See below.++## Usage++```Haskell+import Data.Nbt qualified as Nbt+import Data.Serialize++main :: IO ()+main = do+  shouldBeNbt <- Nbt.readCompressed "level.dat" :: IO (Either String Nbt') -- see also "Nbt.readUncompressed"+  case shouldBeNbt of+    Left err -> putStrLn err+    Right nbt -> do+      print nbt+      Nbt.writeCompressed "anotherlevel.dat" nbt -- see also "Nbt.writeUncompressed"+```++## Possible Future Work++- Drop the `cereal` dependency and do bytestring parsing directly.+- Conversion to/from SNBT.+- Conversion to/from JSON.
+ named-binary-tag.cabal view
@@ -0,0 +1,63 @@+cabal-version:      2.4++name:     named-binary-tag+version:  0.1.0.0+synopsis: NBT (named binary tag) serialization and deserialization.+description: This project was created because the old nbt package has had a 1-liner open issue preventing compilation for 2 years now at the time of writing, despite submitting a pull request to fix it.+    .+    While I was at it, I went ahead and changed the data structure to use konsumlamm's rrb-vector package instead of lists or unboxed arrays for more well-rounded asymptotics.+    .+    StrictData is also turned on, and some functions are exported for reading/writing NBT files that remove some of the old boilerplate code.+category: Data++homepage:    https://github.com/davidgarland/named-binary-tag+bug-reports: https://github.com/davidgarland/named-binary-tag++author: David Garland+maintainer: davidrgarland@me.com++license:      MIT+license-file: LICENSE++tested-with: GHC == 9.2.4, GHC == 9.4.2+extra-source-files: CHANGELOG.md README.md++source-repository head+  type: git+  location: https://github.com/davidgarland/named-binary-tag.git++library+  hs-source-dirs:   src+  exposed-modules: Data.Nbt+  ghc-options: -Wall -O2+  default-extensions: DeriveFunctor+                    , FlexibleContexts+                    , FlexibleInstances+                    , ImportQualifiedPost+                    , LambdaCase+                    , PatternSynonyms+                    , StrictData+  build-depends: base >= 4.16.0 && < 4.18+               , bytestring >= 0.11.3 && < 0.12+               , cereal >= 0.5.8 && < 0.6+               , containers >= 0.6.6 && < 0.7+               , indexed-traversable >= 0.1.2 && < 0.2+               , rrb-vector >= 0.1.1.0 && < 0.2+               , text >= 2.0.1 && < 2.1+               , zlib >= 0.6.3 && < 0.7+  default-language: Haskell2010++test-suite test+  hs-source-dirs: test+  main-is: Main.hs+  ghc-options: -Wall+  type: exitcode-stdio-1.0+  default-language: Haskell2010+  default-extensions: OverloadedStrings+  build-depends: base+               , cereal+               , named-binary-tag+               , tasty+               , tasty-quickcheck+               , text+
+ src/Data/Nbt.hs view
@@ -0,0 +1,285 @@+{-+Module      :  Data.Nbt+Copyright   :  (c) David R. Garland 2022+License     :  MIT+Maintainer  :  davidrgarland@me.com++Functions related to parsing & serializing the NBT file format.+-}++module Data.Nbt+  ( Type (..)+  , Nbt (..)+  , Nbt'+  , MapNbt+  , Tag (..)+  , Tag'+  , MapTag+  , Cmpnd (..)+  , Cmpnd'+  , MapCmpnd+  , pattern Compound'+  , lookupNbt+  , getNbt+  , lookupTag+  , getTag+  , typeOf+  , readUncompressed+  , writeUncompressed+  , readCompressed+  , writeCompressed+  ) where++import Codec.Compression.GZip as GZ+import Control.Monad+import Data.ByteString qualified as B+import Data.ByteString.Lazy qualified as BL+import Data.Foldable+import Data.Foldable.WithIndex+import Data.Int+import Data.Map.Strict qualified as M+import Data.Map.Strict (Map)+import Data.RRBVector qualified as R+import Data.RRBVector (Vector, (<|))+import Data.Serialize hiding (label)+import Data.Text (Text)+import Data.Text.Encoding+import Data.Word++-- | An NBT tag type, in order so that 'toEnum' and 'fromEnum' are useful for serialization and+-- deserialization.+data Type+  = EndType       -- ^ 0x00 NUL. Denotes the end of a file.+  | ByteType      -- ^ 0x01 SOH. Corresponds to `Data.Nbt.Byte`.+  | ShortType     -- ^ 0x02 STX. Corresponds to `Data.Nbt.Short`.+  | IntType       -- ^ 0x03 ETX. Corresponds to `Data.Nbt.Int`.+  | LongType      -- ^ 0x04 EOT. Corresponds to `Data.Nbt.Long`.+  | FloatType     -- ^ 0x05 ENQ. Corresponds to `Data.Nbt.Float`.+  | DoubleType    -- ^ 0x06 ACK. Corresponds to `Data.Nbt.Double`.+  | ByteArrayType -- ^ 0x07 BEL. Corresponds to `ByteArray`.+  | StringType    -- ^ 0x08 BS. Corresponds to `Data.Nbt.String`.+  | ListType      -- ^ 0x09 HT. Corresponds to `List`.+  | CompoundType  -- ^ 0x0a LF. Corresponds to `Compound`.+  | IntArrayType  -- ^ 0x0b VT. Corresponds to `IntArray`.+  | LongArrayType -- ^ 0x0c FF. Corresponds to `LongArray`.+  deriving (Show, Eq, Ord, Enum)++-- | The main NBT type, with support for serialization and deserialization. It couples a 'Text' label with a 'Tag'.+-- +-- When serialized: 1-byte unsigned 'Type' + 2-byte unsigned integer length + N bytes utf-8 text + 'Tag'+data Nbt b = Nbt+  { label :: Text+  , tag :: Tag b+  }+  deriving (Show, Eq, Ord, Functor)++-- | The "simple" form of `Nbt`, which has no label map on `Compound`s.+type Nbt' = Nbt ()++-- | A version of `Nbt` which has a map from labels to element indices for fast lookup.+type MapNbt = Nbt (Map Text Int)++-- | An NBT tag, responsible for storing the actual data.+data Tag b+  = Byte      Int8             -- ^ 1-byte signed integer+  | Short     Int16            -- ^ 2-byte signed integer+  | Int       Int32            -- ^ 4-byte signed integer+  | Long      Int64            -- ^ 8-byte signed integer+  | Float     Float            -- ^ 4-byte float+  | Double    Double           -- ^ 8-byte double+  | ByteArray (Vector Int8)    -- ^ 4-byte signed integer length + N 1-byte signed integers+  | String    Text             -- ^ 2-byte unsigned integer length + N bytes utf-8 text+  | List      (Vector (Tag b)) -- ^ 1-byte unsigned 'Type' + 4-byte signed integer length + N homogenous tags+  | Compound  (Cmpnd b)        -- ^ N heterogenous NBT elements punctuated by an 'EndType'+  | IntArray  (Vector Int32)   -- ^ 4-byte signed integer length + N 4-byte signed integers+  | LongArray (Vector Int64)   -- ^ 4-byte signed integer length + N 8-byte signed integers+  deriving (Show, Eq, Ord, Functor)++-- | A pattern synonym for "Compound (Cmpnd () v)".+pattern Compound' :: Vector Nbt' -> Tag'+pattern Compound' v <- Compound (Cmpnd () v)+  where Compound' v = Compound (Cmpnd () v)+{-# COMPLETE Byte, Short, Int, Long, Float, Double, ByteArray, String, List, Compound', IntArray, LongArray #-}++-- | The "simple" form of `Tag`, which has no label map on `Compound`s.+type Tag' = Tag ()++-- | A version of `Tag` which has a map from labels to element indices for fast lookup.+type MapTag = Tag (Map Text Int)++-- | The payload of the `Compound` constructor.+data Cmpnd b = Cmpnd b (Vector (Nbt b))+  deriving (Show, Eq, Ord, Functor)++-- | The "simple" form of `Cmpnd`, which has no label map.+type Cmpnd' = Cmpnd ()++-- | A version of `Cmpnd` which has a map from labels to element indices for fast lookup.+type MapCmpnd = Cmpnd (Map Text Int)++-- | Use a `MapCmpnd` to quickly look up a `MapNbt` by label.+lookupNbt :: Text -> MapCmpnd -> Maybe MapNbt+lookupNbt t (Cmpnd m v) = (v R.!) <$> M.lookup t m++-- | Use a `MapCmpnd` to quickly get a `MapNbt` by label.+getNbt :: Text -> MapCmpnd -> MapNbt+getNbt t (Cmpnd m v) = v R.! (m M.! t)++-- | Runs `lookupNbt`, then retrieves the `tag`.+lookupTag :: Text -> MapCmpnd -> Maybe MapTag+lookupTag t c = tag <$> lookupNbt t c++-- | Runs `getNbt`, then retrieves the `tag`.+getTag :: Text -> MapCmpnd -> MapTag+getTag t c = tag $ getNbt t c++instance Serialize (Nbt ()) where+  get = get >>= getNbtByType+  put (Nbt n d) = put (typeOf d) >> putString n >> putTag d++instance Serialize (Nbt (Map Text Int)) where+  get = get >>= getNbtByType+  put (Nbt n d) = put (typeOf d) >> putString n >> putTag d++instance Serialize Type where+  get = toEnum . fromIntegral <$> getWord8+  put = putWord8 . fromIntegral . fromEnum++class GenMap b where+  genMap :: Vector (Nbt b) -> b++instance GenMap () where+  genMap = const ()+  {-# INLINE CONLIKE genMap #-}++instance GenMap (Map Text Int) where+  genMap = ifoldr (\i (Nbt l _) a -> M.insert l i a) M.empty+  {-# INLINE genMap #-}++-- | Get the type of an NBT tag.+typeOf :: Tag b -> Type+typeOf = \case+  Byte _ -> ByteType+  Short _ -> ShortType+  Int _ -> IntType+  Long _ -> LongType+  Float _ -> FloatType+  Double _ -> DoubleType+  ByteArray _ -> ByteArrayType+  String _ -> StringType+  List _ -> ListType+  Compound _ -> CompoundType+  IntArray _ -> IntArrayType+  LongArray _ -> LongArrayType++putTag :: Serialize (Nbt b) => Tag b -> Put+putTag = \case+  Byte b -> put b+  Short s -> put s+  Int i -> put i+  Long l -> put l+  Float f -> putFloat32be f+  Double d -> putFloat64be d+  ByteArray bs -> putArray put bs+  String str -> putString str+  List ts -> do+    ty <-+      if null ts then do+        pure EndType+      else do+        let xt = typeOf (ts R.! 0)+        let xs = R.drop 1 ts+        if all (\e -> typeOf e == xt) xs then+          pure xt+        else+          error "attempted to write heterogenous list"+    put ty+    putArray putTag ts+  Compound (Cmpnd _ ts) -> traverse_ put ts >> put EndType+  IntArray is -> putArray put is+  LongArray is -> putArray put is+  where+    putArray :: (e -> Put) -> Vector e -> Put+    putArray putter a = do+      put (fromIntegral (length a) :: Int32)+      traverse_ putter a+{-# SPECIALIZE putTag :: Tag () -> Put #-}+{-# SPECIALIZE putTag :: Tag (Map Text Int) -> Put #-}++getNbtByType :: GenMap b => Type -> Get (Nbt b)+getNbtByType ty = Nbt <$> getString <*> getByType ty+{-# SPECIALIZE getNbtByType :: Type -> Get (Nbt ()) #-}+{-# SPECIALIZE getNbtByType :: Type -> Get (Nbt (Map Text Int)) #-}++getByType :: GenMap b => Type -> Get (Tag b)+getByType = \case+  EndType -> fail "cannot get a value of EndType"+  ByteType -> Byte <$> get+  ShortType -> Short <$> get+  IntType -> Int <$> get+  LongType -> Long <$> get+  FloatType -> Float <$> getFloat32be+  DoubleType -> Double <$> getFloat64be+  ByteArrayType -> ByteArray <$> getArray get+  StringType -> String <$> getString+  ListType -> List <$> (getArray . getByType =<< get)+  CompoundType -> do+    x <- getCompound+    pure $ Compound (Cmpnd (genMap x) x)+  IntArrayType -> IntArray <$> getArray get+  LongArrayType -> LongArray <$> getArray get+  where+    getCompound :: GenMap b => Get (Vector (Nbt b))+    getCompound = get >>= \case+      EndType -> pure R.empty+      ty -> (<|) <$> getNbtByType ty <*> getCompound+{-# SPECIALIZE getByType :: Type -> Get (Tag ()) #-}+{-# SPECIALIZE getByType :: Type -> Get (Tag (Map Text Int)) #-}++{-+-- File IO+-}++-- | Read an uncompressed NBT file. Exceptions are thrown as usual if the internal call to 'Data.ByteString.readFile' fails,+-- and the 'Either' message is that returned by 'Data.Serialize.decode'.+readUncompressed :: Serialize (Nbt b) => FilePath -> IO (Either String (Nbt b))+readUncompressed fp = decode <$> B.readFile fp++-- | Write an uncompressed NBT file.+writeUncompressed :: Serialize (Nbt b) => FilePath -> Nbt b -> IO ()+writeUncompressed fp nbt = B.writeFile fp $ runPut (put nbt)++-- | Read a GZip-compressed NBT file. Exceptions are thrown as usual if the internal call to 'Data.ByteString.Lazy.readFile' fails,+-- and the 'Either' message is that returned by 'Data.Serialize.decode'.+readCompressed :: Serialize (Nbt b) => FilePath -> IO (Either String (Nbt b))+readCompressed fp = do+  bs <- GZ.decompress <$> BL.readFile fp+  pure . decode $ B.toStrict bs++-- | Write a GZip-compressed NBT file.+writeCompressed :: Serialize (Nbt b) => FilePath -> Nbt b -> IO ()+writeCompressed fp nbt = do+  let bs = GZ.compress . BL.fromStrict $ runPut (put nbt)+  B.writeFile fp (B.toStrict bs)++{-+-- Internal Utility Functions+-}++getArray :: Get e -> Get (Vector e)+getArray getter = do+  len <- get :: Get Int32+  elts <- replicateM (fromIntegral len) getter+  pure . R.fromList $ elts++getString :: Get Text+getString = do+  len <- get :: Get Word16+  decodeUtf8 <$> getByteString (fromIntegral len)++putString :: Text -> Put+putString t = do+  let b = encodeUtf8 t+  put (fromIntegral (B.length b) :: Word16)+  putByteString b+
+ test/Main.hs view
@@ -0,0 +1,26 @@+module Main where++import Data.Nbt+import Data.Serialize+import Data.Text+import Test.Tasty+import Test.Tasty.QuickCheck++main = defaultMain tests++tests :: TestTree+tests = testGroup "Tests" [convs]++hmm :: IO ()+hmm = do+  shouldBeNbt <- readCompressed "level.dat" :: IO (Either String Nbt')+  case shouldBeNbt of+    Left err -> putStrLn err+    Right nbt -> do+      print nbt+      writeCompressed "anotherlevel.dat" nbt -- see also "Nbt.writeUncompressed"++convs :: TestTree+convs = testGroup "Conversions"+  [ testProperty "idByte" (\x y -> (Nbt (pack x) (Byte y) :: Nbt') == (either undefined id . runGet get . runPut . put $ (Nbt (pack x) (Byte y) :: Nbt')))+  ]