Jamie Reece Wilson
3732352b4e
[+] AuHashing::Blake2S_32 [+] AuHashing::Blake2S_28 [+] AuHashing::Blake2S_20 [+] AuHashing::Blake2S_16 [+] AuHashing::Blake2B_64 [+] AuHashing::Blake2B_48 [+] AuHashing::Blake2B_32 [+] AuHashing::Blake2B_20 [+] AuHashing::GetHashLength [+] AuHashing::GetHashBits [+] AuHashing::IHashStream::GetHashType
634 lines
20 KiB
C++
634 lines
20 KiB
C++
/***
|
|
Copyright (C) 2021 J Reece Wilson (a/k/a "Reece"). All rights reserved.
|
|
|
|
File: AuHashStream.cpp
|
|
Date: 2021-6-12
|
|
Author: Reece
|
|
***/
|
|
#include <Source/RuntimeInternal.hpp>
|
|
#include <tomcrypt.h>
|
|
#include "AuHashStream.hpp"
|
|
|
|
#if defined(AURORA_COMPILER_CLANG)
|
|
// warning: enumeration values 'kEnumCount' and 'kEnumInvalid' not handled in switch [-Wswitch
|
|
#pragma clang diagnostic ignored "-Wswitch"
|
|
// Yea, I don't give a shit.
|
|
#endif
|
|
|
|
namespace Aurora::Hashing
|
|
{
|
|
#define DIGEST_CHECK(n) SysAssert(n == CRYPT_OK)
|
|
|
|
HashStreamImpl::HashStreamImpl(EHashType type) : type_(type)
|
|
{
|
|
if (!EHashTypeIsValid(type))
|
|
{
|
|
AU_THROW_CONST_STRING("Invalid Hash Type");
|
|
}
|
|
|
|
Init();
|
|
}
|
|
|
|
void HashStreamImpl::Ingest(const Memory::MemoryViewRead &input)
|
|
{
|
|
if (this->bFinished_)
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto buffer = reinterpret_cast<const unsigned char *>(input.ptr);
|
|
auto len = input.length;
|
|
|
|
switch (this->type_)
|
|
{
|
|
case EHashType::eMD4:
|
|
DIGEST_CHECK(md4_process(&this->state_, buffer, len));
|
|
break;
|
|
case EHashType::eMD5:
|
|
DIGEST_CHECK(md5_process(&this->state_, buffer, len));
|
|
break;
|
|
case EHashType::eSHA1:
|
|
DIGEST_CHECK(sha1_process(&this->state_, buffer, len));
|
|
break;
|
|
case EHashType::eSHA2_32:
|
|
DIGEST_CHECK(sha256_process(&this->state_, buffer, len));
|
|
break;
|
|
case EHashType::eSHA2_48:
|
|
DIGEST_CHECK(sha384_process(&this->state_, buffer, len));
|
|
break;
|
|
case EHashType::eSHA2_64:
|
|
DIGEST_CHECK(sha512_process(&this->state_, buffer, len));
|
|
break;
|
|
case EHashType::eSHA3_28:
|
|
case EHashType::eSHA3_48:
|
|
case EHashType::eSHA3_32:
|
|
case EHashType::eSHA3_64:
|
|
DIGEST_CHECK(sha3_process(&this->state_, buffer, len));
|
|
break;
|
|
case EHashType::eTiger:
|
|
DIGEST_CHECK(tiger_process(&this->state_, buffer, len));
|
|
break;
|
|
case EHashType::eRMD128:
|
|
DIGEST_CHECK(rmd128_process(&this->state_, buffer, len));
|
|
break;
|
|
case EHashType::eRMD160:
|
|
DIGEST_CHECK(rmd160_process(&this->state_, buffer, len));
|
|
break;
|
|
case EHashType::eRMD256:
|
|
DIGEST_CHECK(rmd256_process(&this->state_, buffer, len));
|
|
break;
|
|
case EHashType::eRMD320:
|
|
DIGEST_CHECK(rmd320_process(&this->state_, buffer, len));
|
|
break;
|
|
case EHashType::eWhirlpool:
|
|
DIGEST_CHECK(whirlpool_process(&this->state_, buffer, len));
|
|
break;
|
|
case EHashType::eBlake2S_32:
|
|
case EHashType::eBlake2S_28:
|
|
case EHashType::eBlake2S_20:
|
|
case EHashType::eBlake2S_16:
|
|
DIGEST_CHECK(blake2s_process(&this->state_, buffer, len));
|
|
break;
|
|
case EHashType::eBlake2B_64:
|
|
case EHashType::eBlake2B_48:
|
|
case EHashType::eBlake2B_32:
|
|
case EHashType::eBlake2B_20:
|
|
DIGEST_CHECK(blake2b_process(&this->state_, buffer, len));
|
|
break;
|
|
}
|
|
}
|
|
|
|
AuUInt8 const* HashStreamImpl::GetBytes(AuUInt32 &length)
|
|
{
|
|
switch (this->type_)
|
|
{
|
|
case EHashType::eMD4:
|
|
length = 16;
|
|
if (!AuExchange(this->bFinished_, true))
|
|
{
|
|
DIGEST_CHECK(md4_done(&this->state_, reinterpret_cast<unsigned char *>(this->buffer_)));
|
|
}
|
|
return this->buffer_;
|
|
|
|
case EHashType::eMD5:
|
|
length = 16;
|
|
if (!AuExchange(this->bFinished_, true))
|
|
{
|
|
DIGEST_CHECK(md5_done(&this->state_, reinterpret_cast<unsigned char *>(this->buffer_)));
|
|
}
|
|
return this->buffer_;
|
|
|
|
case EHashType::eSHA1:
|
|
length = 20;
|
|
if (!AuExchange(this->bFinished_, true))
|
|
{
|
|
DIGEST_CHECK(sha1_done(&this->state_, reinterpret_cast<unsigned char *>(this->buffer_)));
|
|
}
|
|
return this->buffer_;
|
|
|
|
case EHashType::eSHA2_48:
|
|
length = 48;
|
|
if (!AuExchange(this->bFinished_, true))
|
|
{
|
|
DIGEST_CHECK(sha384_done(&this->state_, reinterpret_cast<unsigned char *>(this->buffer_)));
|
|
}
|
|
return this->buffer_;
|
|
|
|
case EHashType::eSHA2_32:
|
|
length = 32;
|
|
if (!AuExchange(this->bFinished_, true))
|
|
{
|
|
DIGEST_CHECK(sha256_done(&this->state_, reinterpret_cast<unsigned char *>(this->buffer_)));
|
|
}
|
|
return this->buffer_;
|
|
|
|
case EHashType::eSHA2_64:
|
|
length = 64;
|
|
if (!AuExchange(this->bFinished_, true))
|
|
{
|
|
DIGEST_CHECK(sha512_done(&this->state_, reinterpret_cast<unsigned char *>(this->buffer_)));
|
|
}
|
|
return this->buffer_;
|
|
|
|
case EHashType::eSHA3_28:
|
|
length = 28;
|
|
if (!AuExchange(this->bFinished_, true))
|
|
{
|
|
DIGEST_CHECK(sha3_done(&this->state_, reinterpret_cast<unsigned char *>(this->buffer_)));
|
|
}
|
|
return this->buffer_;
|
|
|
|
case EHashType::eSHA3_48:
|
|
length = 48;
|
|
if (!AuExchange(this->bFinished_, true))
|
|
{
|
|
DIGEST_CHECK(sha3_done(&this->state_, reinterpret_cast<unsigned char *>(this->buffer_)));
|
|
}
|
|
return this->buffer_;
|
|
|
|
case EHashType::eSHA3_32:
|
|
length = 32;
|
|
if (!AuExchange(this->bFinished_, true))
|
|
{
|
|
DIGEST_CHECK(sha3_done(&this->state_, reinterpret_cast<unsigned char *>(this->buffer_)));
|
|
}
|
|
return this->buffer_;
|
|
|
|
case EHashType::eSHA3_64:
|
|
length = 64;
|
|
if (!AuExchange(this->bFinished_, true))
|
|
{
|
|
DIGEST_CHECK(sha3_done(&this->state_, reinterpret_cast<unsigned char *>(this->buffer_)));
|
|
}
|
|
return this->buffer_;
|
|
|
|
case EHashType::eTiger:
|
|
length = 24;
|
|
if (!AuExchange(this->bFinished_, true))
|
|
{
|
|
DIGEST_CHECK(tiger_done(&this->state_, reinterpret_cast<unsigned char *>(this->buffer_)));
|
|
}
|
|
return this->buffer_;
|
|
|
|
case EHashType::eRMD128:
|
|
length = 16;
|
|
if (!AuExchange(this->bFinished_, true))
|
|
{
|
|
DIGEST_CHECK(rmd128_done(&this->state_, reinterpret_cast<unsigned char *>(this->buffer_)));
|
|
}
|
|
return this->buffer_;
|
|
|
|
case EHashType::eRMD160:
|
|
length = 20;
|
|
if (!AuExchange(this->bFinished_, true))
|
|
{
|
|
DIGEST_CHECK(rmd160_done(&this->state_, reinterpret_cast<unsigned char *>(this->buffer_)));
|
|
}
|
|
return this->buffer_;
|
|
|
|
case EHashType::eRMD256:
|
|
length = 32;
|
|
if (!AuExchange(this->bFinished_, true))
|
|
{
|
|
DIGEST_CHECK(rmd256_done(&this->state_, reinterpret_cast<unsigned char *>(this->buffer_)));
|
|
}
|
|
return this->buffer_;
|
|
|
|
case EHashType::eRMD320:
|
|
length = 40;
|
|
|
|
if (!AuExchange(this->bFinished_, true))
|
|
{
|
|
DIGEST_CHECK(rmd320_done(&this->state_, reinterpret_cast<unsigned char *>(this->buffer_)));
|
|
}
|
|
return this->buffer_;
|
|
|
|
case EHashType::eWhirlpool:
|
|
length = 64;
|
|
|
|
if (!AuExchange(this->bFinished_, true))
|
|
{
|
|
DIGEST_CHECK(whirlpool_done(&this->state_, reinterpret_cast<unsigned char *>(this->buffer_)));
|
|
}
|
|
return this->buffer_;
|
|
|
|
case EHashType::eBlake2S_32:
|
|
length = 32;
|
|
|
|
if (!AuExchange(this->bFinished_, true))
|
|
{
|
|
DIGEST_CHECK(blake2s_done(&this->state_, reinterpret_cast<unsigned char *>(this->buffer_)));
|
|
}
|
|
return this->buffer_;
|
|
|
|
case EHashType::eBlake2S_28:
|
|
length = 28;
|
|
|
|
if (!AuExchange(this->bFinished_, true))
|
|
{
|
|
DIGEST_CHECK(blake2s_done(&this->state_, reinterpret_cast<unsigned char *>(this->buffer_)));
|
|
}
|
|
return this->buffer_;
|
|
|
|
case EHashType::eBlake2S_20:
|
|
length = 20;
|
|
|
|
if (!AuExchange(this->bFinished_, true))
|
|
{
|
|
DIGEST_CHECK(blake2s_done(&this->state_, reinterpret_cast<unsigned char *>(this->buffer_)));
|
|
}
|
|
return this->buffer_;
|
|
|
|
case EHashType::eBlake2S_16:
|
|
length = 16;
|
|
|
|
if (!AuExchange(this->bFinished_, true))
|
|
{
|
|
DIGEST_CHECK(blake2s_done(&this->state_, reinterpret_cast<unsigned char *>(this->buffer_)));
|
|
}
|
|
return this->buffer_;
|
|
|
|
case EHashType::eBlake2B_64:
|
|
length = 64;
|
|
|
|
if (!AuExchange(this->bFinished_, true))
|
|
{
|
|
DIGEST_CHECK(blake2b_done(&this->state_, reinterpret_cast<unsigned char *>(this->buffer_)));
|
|
}
|
|
return this->buffer_;
|
|
|
|
case EHashType::eBlake2B_48:
|
|
length = 48;
|
|
|
|
if (!AuExchange(this->bFinished_, true))
|
|
{
|
|
DIGEST_CHECK(blake2b_done(&this->state_, reinterpret_cast<unsigned char *>(this->buffer_)));
|
|
}
|
|
return this->buffer_;
|
|
|
|
case EHashType::eBlake2B_32:
|
|
length = 32;
|
|
|
|
if (!AuExchange(this->bFinished_, true))
|
|
{
|
|
DIGEST_CHECK(blake2b_done(&this->state_, reinterpret_cast<unsigned char *>(this->buffer_)));
|
|
}
|
|
return this->buffer_;
|
|
|
|
case EHashType::eBlake2B_20:
|
|
length = 20;
|
|
|
|
if (!AuExchange(this->bFinished_, true))
|
|
{
|
|
DIGEST_CHECK(blake2b_done(&this->state_, reinterpret_cast<unsigned char *>(this->buffer_)));
|
|
}
|
|
return this->buffer_;
|
|
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
AuMemoryViewRead HashStreamImpl::Finalize()
|
|
{
|
|
AuUInt32 length;
|
|
auto begin = GetBytes(length);
|
|
return AuMemoryViewRead(begin, length);
|
|
}
|
|
|
|
Memory::MemoryViewRead HashStreamImpl::PeekFinalize()
|
|
{
|
|
if (this->bFinished_)
|
|
{
|
|
return this->Finalize();
|
|
}
|
|
|
|
hash_state state;
|
|
AuMemcpy(&state, &this->state_, sizeof(state));
|
|
auto view = this->Finalize();
|
|
AuMemcpy(&this->state_, &state, sizeof(state));
|
|
this->bFinished_ = false;
|
|
|
|
return view;
|
|
}
|
|
|
|
AuResult<AuMemoryViewRead> HashStreamImpl::Export()
|
|
{
|
|
// Defer to HASH_PROCESS defined in the private libtomcrypt header
|
|
|
|
//this->state_.name.length | bits processed
|
|
|
|
#define ADD_EXPORT(name) \
|
|
if (this->state_.name.curlen) \
|
|
{ \
|
|
return {}; \
|
|
} \
|
|
return AuMemoryViewRead {this->state_.name.state, sizeof(this->state_.name.state)};
|
|
|
|
#define ADD_EXPORT_UNSAFE(name) \
|
|
if (this->state_.name.curlen) \
|
|
{ \
|
|
return {}; \
|
|
} \
|
|
return AuMemoryViewRead {&this->state_.name, sizeof(this->state_.name)};
|
|
|
|
switch (this->type_)
|
|
{
|
|
case EHashType::eMD4:
|
|
ADD_EXPORT(md4);
|
|
|
|
case EHashType::eMD5:
|
|
ADD_EXPORT(md5);
|
|
|
|
case EHashType::eSHA1:
|
|
ADD_EXPORT(sha1);
|
|
|
|
case EHashType::eSHA2_32:
|
|
ADD_EXPORT(sha256);
|
|
|
|
case EHashType::eSHA2_48:
|
|
case EHashType::eSHA2_64:
|
|
ADD_EXPORT(sha512);
|
|
|
|
case EHashType::eSHA3_28:
|
|
case EHashType::eSHA3_48:
|
|
case EHashType::eSHA3_32:
|
|
case EHashType::eSHA3_64:
|
|
{
|
|
if (this->state_.sha3.byte_index)
|
|
{
|
|
return {};
|
|
}
|
|
|
|
return AuMemoryViewRead(this->state_.sha3.s, &this->state_.sha3.s[this->state_.sha3.word_index]);
|
|
}
|
|
|
|
case EHashType::eTiger:
|
|
ADD_EXPORT(tiger);
|
|
|
|
case EHashType::eRMD128:
|
|
ADD_EXPORT(rmd128);
|
|
|
|
case EHashType::eRMD160:
|
|
ADD_EXPORT(rmd160);
|
|
|
|
case EHashType::eRMD256:
|
|
ADD_EXPORT(rmd256);
|
|
|
|
case EHashType::eRMD320:
|
|
ADD_EXPORT(rmd320);
|
|
|
|
case EHashType::eWhirlpool:
|
|
ADD_EXPORT(whirlpool);
|
|
|
|
case EHashType::eBlake2S_32:
|
|
case EHashType::eBlake2S_28:
|
|
case EHashType::eBlake2S_20:
|
|
case EHashType::eBlake2S_16:
|
|
ADD_EXPORT_UNSAFE(blake2s)
|
|
|
|
case EHashType::eBlake2B_64:
|
|
case EHashType::eBlake2B_48:
|
|
case EHashType::eBlake2B_32:
|
|
case EHashType::eBlake2B_20:
|
|
ADD_EXPORT_UNSAFE(blake2b)
|
|
|
|
|
|
default:
|
|
return {};
|
|
}
|
|
#undef ADD_EXPORT
|
|
}
|
|
|
|
bool HashStreamImpl::Import(const Memory::MemoryViewRead &view)
|
|
{
|
|
this->bFinished_ = false;
|
|
|
|
#define ADD_IMPORT(name) \
|
|
if (sizeof(this->state_.name.state) != view.length) \
|
|
{ \
|
|
SysPushErrorCrypt("Invalid hash state length -> mixed ciphers?"); \
|
|
return false; \
|
|
} \
|
|
AuMemset(&this->state_, 0, sizeof(this->state_)); \
|
|
AuMemcpy(this->state_.name.state, view.ptr, sizeof(this->state_.name.state)); \
|
|
return true;
|
|
|
|
#define ADD_IMPORT_UNSAFE(name) \
|
|
if (sizeof(this->state_.name) != view.length) \
|
|
{ \
|
|
SysPushErrorCrypt("Invalid hash state length -> mixed ciphers?"); \
|
|
return false; \
|
|
} \
|
|
if (this->state_.name.outlen != ((decltype(this->state_.name) *)(view.ptr))->outlen) \
|
|
{ \
|
|
SysPushErrorCrypt("Invalid hash state length -> mixed ciphers?"); \
|
|
return false; \
|
|
} \
|
|
AuMemset(&this->state_, 0, sizeof(this->state_)); \
|
|
AuMemcpy(&this->state_.name, view.ptr, sizeof(this->state_.name)); \
|
|
return true;
|
|
|
|
switch (this->type_)
|
|
{
|
|
case EHashType::eMD4:
|
|
ADD_IMPORT(md4);
|
|
|
|
case EHashType::eMD5:
|
|
ADD_IMPORT(md5);
|
|
|
|
case EHashType::eSHA1:
|
|
ADD_IMPORT(sha1);
|
|
|
|
case EHashType::eSHA2_32:
|
|
ADD_IMPORT(sha256);
|
|
|
|
case EHashType::eSHA2_48:
|
|
case EHashType::eSHA2_64:
|
|
ADD_IMPORT(sha512);
|
|
|
|
case EHashType::eSHA3_28:
|
|
case EHashType::eSHA3_48:
|
|
case EHashType::eSHA3_32:
|
|
case EHashType::eSHA3_64:
|
|
{
|
|
if (this->state_.sha3.byte_index)
|
|
{
|
|
return {};
|
|
}
|
|
|
|
if (sizeof(this->state_.sha3.s) < view.length)
|
|
{
|
|
return {};
|
|
}
|
|
|
|
AuMemcpy(this->state_.sha3.s, view.ptr, view.length);
|
|
this->state_.sha3.word_index = view.length / 8;
|
|
return true;
|
|
}
|
|
|
|
case EHashType::eTiger:
|
|
ADD_IMPORT(tiger);
|
|
|
|
case EHashType::eRMD128:
|
|
ADD_IMPORT(rmd128);
|
|
|
|
case EHashType::eRMD160:
|
|
ADD_IMPORT(rmd160);
|
|
|
|
case EHashType::eRMD256:
|
|
ADD_IMPORT(rmd256);
|
|
|
|
case EHashType::eRMD320:
|
|
ADD_IMPORT(rmd320);
|
|
|
|
case EHashType::eWhirlpool:
|
|
ADD_IMPORT(whirlpool);
|
|
|
|
case EHashType::eBlake2S_32:
|
|
case EHashType::eBlake2S_28:
|
|
case EHashType::eBlake2S_20:
|
|
case EHashType::eBlake2S_16:
|
|
ADD_IMPORT_UNSAFE(blake2s)
|
|
|
|
case EHashType::eBlake2B_64:
|
|
case EHashType::eBlake2B_48:
|
|
case EHashType::eBlake2B_32:
|
|
case EHashType::eBlake2B_20:
|
|
ADD_IMPORT_UNSAFE(blake2b)
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
#undef ADD_IMPORT
|
|
}
|
|
|
|
void HashStreamImpl::Reset()
|
|
{
|
|
Init();
|
|
}
|
|
|
|
void HashStreamImpl::Init()
|
|
{
|
|
this->bFinished_ = false;
|
|
AuMemset(&this->state_, 0, sizeof(this->state_));
|
|
|
|
switch (this->type_)
|
|
{
|
|
case EHashType::eMD4:
|
|
DIGEST_CHECK(md4_init(&this->state_));
|
|
break;
|
|
case EHashType::eMD5:
|
|
DIGEST_CHECK(md5_init(&this->state_));
|
|
break;
|
|
case EHashType::eSHA1:
|
|
DIGEST_CHECK(sha1_init(&this->state_));
|
|
break;
|
|
case EHashType::eSHA2_32:
|
|
DIGEST_CHECK(sha256_init(&this->state_));
|
|
break;
|
|
case EHashType::eSHA2_48:
|
|
DIGEST_CHECK(sha384_init(&this->state_));
|
|
break;
|
|
case EHashType::eSHA2_64:
|
|
DIGEST_CHECK(sha512_init(&this->state_));
|
|
break;
|
|
case EHashType::eSHA3_28:
|
|
DIGEST_CHECK(sha3_224_init(&this->state_));
|
|
break;
|
|
case EHashType::eSHA3_32:
|
|
DIGEST_CHECK(sha3_256_init(&this->state_));
|
|
break;
|
|
case EHashType::eSHA3_48:
|
|
DIGEST_CHECK(sha3_384_init(&this->state_));
|
|
break;
|
|
case EHashType::eSHA3_64:
|
|
DIGEST_CHECK(sha3_512_init(&this->state_));
|
|
break;
|
|
case EHashType::eTiger:
|
|
DIGEST_CHECK(tiger_init(&this->state_));
|
|
break;
|
|
case EHashType::eRMD128:
|
|
DIGEST_CHECK(rmd128_init(&this->state_));
|
|
break;
|
|
case EHashType::eRMD160:
|
|
DIGEST_CHECK(rmd160_init(&this->state_));
|
|
break;
|
|
case EHashType::eRMD256:
|
|
DIGEST_CHECK(rmd256_init(&this->state_));
|
|
break;
|
|
case EHashType::eRMD320:
|
|
DIGEST_CHECK(rmd320_init(&this->state_));
|
|
break;
|
|
case EHashType::eWhirlpool:
|
|
DIGEST_CHECK(whirlpool_init(&this->state_));
|
|
break;
|
|
case EHashType::eBlake2S_32:
|
|
DIGEST_CHECK(blake2s_256_init(&this->state_));
|
|
break;
|
|
case EHashType::eBlake2S_28:
|
|
DIGEST_CHECK(blake2s_224_init(&this->state_));
|
|
break;
|
|
case EHashType::eBlake2S_20:
|
|
DIGEST_CHECK(blake2s_160_init(&this->state_));
|
|
break;
|
|
case EHashType::eBlake2S_16:
|
|
DIGEST_CHECK(blake2s_128_init(&this->state_));
|
|
break;
|
|
case EHashType::eBlake2B_64:
|
|
DIGEST_CHECK(blake2b_512_init(&this->state_));
|
|
break;
|
|
case EHashType::eBlake2B_48:
|
|
DIGEST_CHECK(blake2b_384_init(&this->state_));
|
|
break;
|
|
case EHashType::eBlake2B_32:
|
|
DIGEST_CHECK(blake2b_256_init(&this->state_));
|
|
break;
|
|
case EHashType::eBlake2B_20:
|
|
DIGEST_CHECK(blake2b_160_init(&this->state_));
|
|
break;
|
|
}
|
|
}
|
|
|
|
EHashType HashStreamImpl::GetHashType()
|
|
{
|
|
return this->type_;
|
|
}
|
|
|
|
AUKN_SYM IHashStream *HashStreamNew(EHashType type)
|
|
{
|
|
if (!EHashTypeIsValid(type))
|
|
{
|
|
return {};
|
|
}
|
|
|
|
return _new HashStreamImpl(type);
|
|
}
|
|
|
|
AUKN_SYM void HashStreamRelease(IHashStream *pStream)
|
|
{
|
|
AuSafeDelete<HashStreamImpl *>(pStream);
|
|
}
|
|
|
|
AUROXTL_INTERFACE_SOO_SRC(HashStream, HashStreamImpl, (EHashType, type))
|
|
} |