These functions create an intermediate list. For large parquet files this is a lot of overhead. We can pre-allocate some space and create a bytestrin directly.
readByteString :: BS.ByteString -> IORef Int -> IO BS.ByteString
readByteString buf pos = do
size <- readVarIntFromBuffer @Int buf pos
BS.pack <$> replicateM size (readAndAdvance pos buf)
readByteString' :: BS.ByteString -> Int64 -> IO BS.ByteString
readByteString' buf size = BS.pack <$> mapM (`readSingleByte` buf) [0 .. (size - 1)]