2017-07-05 10:28:15 +00:00
|
|
|
pragma solidity ^0.4.6;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @title RLPReader
|
|
|
|
*
|
|
|
|
* RLPReader is used to read and parse RLP encoded data in memory.
|
|
|
|
*
|
|
|
|
* @author Andreas Olofsson (androlo1980@gmail.com)
|
|
|
|
*/
|
|
|
|
library RLP {
|
|
|
|
|
|
|
|
uint constant DATA_SHORT_START = 0x80;
|
|
|
|
uint constant DATA_LONG_START = 0xB8;
|
|
|
|
uint constant LIST_SHORT_START = 0xC0;
|
|
|
|
uint constant LIST_LONG_START = 0xF8;
|
|
|
|
|
|
|
|
uint constant DATA_LONG_OFFSET = 0xB7;
|
|
|
|
uint constant LIST_LONG_OFFSET = 0xF7;
|
|
|
|
|
|
|
|
|
|
|
|
struct RLPItem {
|
|
|
|
uint _unsafe_memPtr; // Pointer to the RLP-encoded bytes.
|
|
|
|
uint _unsafe_length; // Number of bytes. This is the full length of the string.
|
|
|
|
}
|
|
|
|
|
|
|
|
struct Iterator {
|
|
|
|
RLPItem _unsafe_item; // Item that's being iterated over.
|
|
|
|
uint _unsafe_nextPtr; // Position of the next item in the list.
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Iterator */
|
|
|
|
|
2018-07-02 09:14:28 +00:00
|
|
|
function next(Iterator memory self) internal view returns (RLPItem memory subItem) {
|
2017-07-05 10:28:15 +00:00
|
|
|
if(hasNext(self)) {
|
2018-06-21 11:58:38 +00:00
|
|
|
uint ptr = self._unsafe_nextPtr;
|
|
|
|
uint itemLength = _itemLength(ptr);
|
2017-07-05 10:28:15 +00:00
|
|
|
subItem._unsafe_memPtr = ptr;
|
|
|
|
subItem._unsafe_length = itemLength;
|
|
|
|
self._unsafe_nextPtr = ptr + itemLength;
|
|
|
|
}
|
|
|
|
else
|
2018-07-11 23:49:00 +00:00
|
|
|
revert();
|
2017-07-05 10:28:15 +00:00
|
|
|
}
|
|
|
|
|
2018-07-02 09:14:28 +00:00
|
|
|
function next(Iterator memory self, bool strict) internal view returns (RLPItem memory subItem) {
|
2017-07-05 10:28:15 +00:00
|
|
|
subItem = next(self);
|
|
|
|
if(strict && !_validate(subItem))
|
2018-07-11 23:49:00 +00:00
|
|
|
revert();
|
2017-07-05 10:28:15 +00:00
|
|
|
}
|
|
|
|
|
2018-07-02 09:14:28 +00:00
|
|
|
function hasNext(Iterator memory self) internal view returns (bool) {
|
2018-06-21 11:58:38 +00:00
|
|
|
RLPItem memory item = self._unsafe_item;
|
2017-07-05 10:28:15 +00:00
|
|
|
return self._unsafe_nextPtr < item._unsafe_memPtr + item._unsafe_length;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* RLPItem */
|
|
|
|
|
|
|
|
/// @dev Creates an RLPItem from an array of RLP encoded bytes.
|
|
|
|
/// @param self The RLP encoded bytes.
|
|
|
|
/// @return An RLPItem
|
2018-07-02 09:14:28 +00:00
|
|
|
function toRLPItem(bytes memory self) internal view returns (RLPItem memory) {
|
2017-07-05 10:28:15 +00:00
|
|
|
uint len = self.length;
|
|
|
|
if (len == 0) {
|
|
|
|
return RLPItem(0, 0);
|
|
|
|
}
|
|
|
|
uint memPtr;
|
|
|
|
assembly {
|
|
|
|
memPtr := add(self, 0x20)
|
|
|
|
}
|
|
|
|
return RLPItem(memPtr, len);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// @dev Creates an RLPItem from an array of RLP encoded bytes.
|
|
|
|
/// @param self The RLP encoded bytes.
|
|
|
|
/// @param strict Will throw if the data is not RLP encoded.
|
|
|
|
/// @return An RLPItem
|
2018-07-02 09:14:28 +00:00
|
|
|
function toRLPItem(bytes memory self, bool strict) internal view returns (RLPItem memory) {
|
2018-06-21 11:58:38 +00:00
|
|
|
RLPItem memory item = toRLPItem(self);
|
2017-07-05 10:28:15 +00:00
|
|
|
if(strict) {
|
|
|
|
uint len = self.length;
|
|
|
|
if(_payloadOffset(item) > len)
|
2018-07-11 23:49:00 +00:00
|
|
|
revert();
|
2017-07-05 10:28:15 +00:00
|
|
|
if(_itemLength(item._unsafe_memPtr) != len)
|
2018-07-11 23:49:00 +00:00
|
|
|
revert();
|
2017-07-05 10:28:15 +00:00
|
|
|
if(!_validate(item))
|
2018-07-11 23:49:00 +00:00
|
|
|
revert();
|
2017-07-05 10:28:15 +00:00
|
|
|
}
|
|
|
|
return item;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// @dev Check if the RLP item is null.
|
|
|
|
/// @param self The RLP item.
|
|
|
|
/// @return 'true' if the item is null.
|
2018-07-02 09:14:28 +00:00
|
|
|
function isNull(RLPItem memory self) internal view returns (bool ret) {
|
2017-07-05 10:28:15 +00:00
|
|
|
return self._unsafe_length == 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// @dev Check if the RLP item is a list.
|
|
|
|
/// @param self The RLP item.
|
|
|
|
/// @return 'true' if the item is a list.
|
2018-07-02 09:14:28 +00:00
|
|
|
function isList(RLPItem memory self) internal view returns (bool ret) {
|
2017-07-05 10:28:15 +00:00
|
|
|
if (self._unsafe_length == 0)
|
|
|
|
return false;
|
|
|
|
uint memPtr = self._unsafe_memPtr;
|
|
|
|
assembly {
|
|
|
|
ret := iszero(lt(byte(0, mload(memPtr)), 0xC0))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// @dev Check if the RLP item is data.
|
|
|
|
/// @param self The RLP item.
|
|
|
|
/// @return 'true' if the item is data.
|
2018-07-02 09:14:28 +00:00
|
|
|
function isData(RLPItem memory self) internal view returns (bool ret) {
|
2017-07-05 10:28:15 +00:00
|
|
|
if (self._unsafe_length == 0)
|
|
|
|
return false;
|
|
|
|
uint memPtr = self._unsafe_memPtr;
|
|
|
|
assembly {
|
|
|
|
ret := lt(byte(0, mload(memPtr)), 0xC0)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// @dev Check if the RLP item is empty (string or list).
|
|
|
|
/// @param self The RLP item.
|
|
|
|
/// @return 'true' if the item is null.
|
2018-07-02 09:14:28 +00:00
|
|
|
function isEmpty(RLPItem memory self) internal view returns (bool ret) {
|
2017-07-05 10:28:15 +00:00
|
|
|
if(isNull(self))
|
|
|
|
return false;
|
|
|
|
uint b0;
|
|
|
|
uint memPtr = self._unsafe_memPtr;
|
|
|
|
assembly {
|
|
|
|
b0 := byte(0, mload(memPtr))
|
|
|
|
}
|
|
|
|
return (b0 == DATA_SHORT_START || b0 == LIST_SHORT_START);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// @dev Get the number of items in an RLP encoded list.
|
|
|
|
/// @param self The RLP item.
|
|
|
|
/// @return The number of items.
|
2018-07-02 09:14:28 +00:00
|
|
|
function items(RLPItem memory self) internal view returns (uint) {
|
2017-07-05 10:28:15 +00:00
|
|
|
if (!isList(self))
|
|
|
|
return 0;
|
|
|
|
uint b0;
|
|
|
|
uint memPtr = self._unsafe_memPtr;
|
|
|
|
assembly {
|
|
|
|
b0 := byte(0, mload(memPtr))
|
|
|
|
}
|
|
|
|
uint pos = memPtr + _payloadOffset(self);
|
|
|
|
uint last = memPtr + self._unsafe_length - 1;
|
|
|
|
uint itms;
|
|
|
|
while(pos <= last) {
|
|
|
|
pos += _itemLength(pos);
|
|
|
|
itms++;
|
|
|
|
}
|
|
|
|
return itms;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// @dev Create an iterator.
|
|
|
|
/// @param self The RLP item.
|
|
|
|
/// @return An 'Iterator' over the item.
|
2018-07-02 09:14:28 +00:00
|
|
|
function iterator(RLPItem memory self) internal view returns (Iterator memory it) {
|
2017-07-05 10:28:15 +00:00
|
|
|
if (!isList(self))
|
2018-07-11 23:49:00 +00:00
|
|
|
revert();
|
2017-07-05 10:28:15 +00:00
|
|
|
uint ptr = self._unsafe_memPtr + _payloadOffset(self);
|
|
|
|
it._unsafe_item = self;
|
|
|
|
it._unsafe_nextPtr = ptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// @dev Return the RLP encoded bytes.
|
|
|
|
/// @param self The RLPItem.
|
|
|
|
/// @return The bytes.
|
2018-07-02 09:14:28 +00:00
|
|
|
function toBytes(RLPItem memory self) internal returns (bytes memory bts) {
|
2018-06-21 11:58:38 +00:00
|
|
|
uint len = self._unsafe_length;
|
2018-08-07 18:47:52 +00:00
|
|
|
if (len != 0)
|
|
|
|
{
|
|
|
|
bts = new bytes(len);
|
|
|
|
_copyToBytes(self._unsafe_memPtr, bts, len);
|
|
|
|
}
|
2017-07-05 10:28:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// @dev Decode an RLPItem into bytes. This will not work if the
|
|
|
|
/// RLPItem is a list.
|
|
|
|
/// @param self The RLPItem.
|
|
|
|
/// @return The decoded string.
|
2018-07-02 09:14:28 +00:00
|
|
|
function toData(RLPItem memory self) internal returns (bytes memory bts) {
|
2017-07-05 10:28:15 +00:00
|
|
|
if(!isData(self))
|
2018-07-11 23:49:00 +00:00
|
|
|
revert();
|
2018-06-21 11:58:38 +00:00
|
|
|
(uint rStartPos, uint len) = _decode(self);
|
2017-07-05 10:28:15 +00:00
|
|
|
bts = new bytes(len);
|
|
|
|
_copyToBytes(rStartPos, bts, len);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// @dev Get the list of sub-items from an RLP encoded list.
|
|
|
|
/// Warning: This is inefficient, as it requires that the list is read twice.
|
|
|
|
/// @param self The RLP item.
|
|
|
|
/// @return Array of RLPItems.
|
2018-07-02 09:14:28 +00:00
|
|
|
function toList(RLPItem memory self) internal view returns (RLPItem[] memory list) {
|
2017-07-05 10:28:15 +00:00
|
|
|
if(!isList(self))
|
2018-07-11 23:49:00 +00:00
|
|
|
revert();
|
2018-06-21 11:58:38 +00:00
|
|
|
uint numItems = items(self);
|
2017-07-05 10:28:15 +00:00
|
|
|
list = new RLPItem[](numItems);
|
2018-06-21 11:58:38 +00:00
|
|
|
Iterator memory it = iterator(self);
|
2017-07-05 10:28:15 +00:00
|
|
|
uint idx;
|
|
|
|
while(hasNext(it)) {
|
|
|
|
list[idx] = next(it);
|
|
|
|
idx++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// @dev Decode an RLPItem into an ascii string. This will not work if the
|
|
|
|
/// RLPItem is a list.
|
|
|
|
/// @param self The RLPItem.
|
|
|
|
/// @return The decoded string.
|
2018-07-02 09:14:28 +00:00
|
|
|
function toAscii(RLPItem memory self) internal returns (string memory str) {
|
2017-07-05 10:28:15 +00:00
|
|
|
if(!isData(self))
|
2018-07-11 23:49:00 +00:00
|
|
|
revert();
|
2018-06-21 11:58:38 +00:00
|
|
|
(uint rStartPos, uint len) = _decode(self);
|
2017-07-05 10:28:15 +00:00
|
|
|
bytes memory bts = new bytes(len);
|
|
|
|
_copyToBytes(rStartPos, bts, len);
|
|
|
|
str = string(bts);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// @dev Decode an RLPItem into a uint. This will not work if the
|
|
|
|
/// RLPItem is a list.
|
|
|
|
/// @param self The RLPItem.
|
|
|
|
/// @return The decoded string.
|
2018-07-02 09:14:28 +00:00
|
|
|
function toUint(RLPItem memory self) internal view returns (uint data) {
|
2017-07-05 10:28:15 +00:00
|
|
|
if(!isData(self))
|
2018-07-11 23:49:00 +00:00
|
|
|
revert();
|
2018-06-21 11:58:38 +00:00
|
|
|
(uint rStartPos, uint len) = _decode(self);
|
2017-07-05 10:28:15 +00:00
|
|
|
if (len > 32 || len == 0)
|
2018-07-11 23:49:00 +00:00
|
|
|
revert();
|
2017-07-05 10:28:15 +00:00
|
|
|
assembly {
|
|
|
|
data := div(mload(rStartPos), exp(256, sub(32, len)))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// @dev Decode an RLPItem into a boolean. This will not work if the
|
|
|
|
/// RLPItem is a list.
|
|
|
|
/// @param self The RLPItem.
|
|
|
|
/// @return The decoded string.
|
2018-07-02 09:14:28 +00:00
|
|
|
function toBool(RLPItem memory self) internal view returns (bool data) {
|
2017-07-05 10:28:15 +00:00
|
|
|
if(!isData(self))
|
2018-07-11 23:49:00 +00:00
|
|
|
revert();
|
2018-06-21 11:58:38 +00:00
|
|
|
(uint rStartPos, uint len) = _decode(self);
|
2017-07-05 10:28:15 +00:00
|
|
|
if (len != 1)
|
2018-07-11 23:49:00 +00:00
|
|
|
revert();
|
2017-07-05 10:28:15 +00:00
|
|
|
uint temp;
|
|
|
|
assembly {
|
|
|
|
temp := byte(0, mload(rStartPos))
|
|
|
|
}
|
|
|
|
if (temp > 1)
|
2018-07-11 23:49:00 +00:00
|
|
|
revert();
|
2017-07-05 10:28:15 +00:00
|
|
|
return temp == 1 ? true : false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// @dev Decode an RLPItem into a byte. This will not work if the
|
|
|
|
/// RLPItem is a list.
|
|
|
|
/// @param self The RLPItem.
|
|
|
|
/// @return The decoded string.
|
2018-07-02 09:14:28 +00:00
|
|
|
function toByte(RLPItem memory self) internal view returns (byte data) {
|
2017-07-05 10:28:15 +00:00
|
|
|
if(!isData(self))
|
2018-07-11 23:49:00 +00:00
|
|
|
revert();
|
2018-06-21 11:58:38 +00:00
|
|
|
(uint rStartPos, uint len) = _decode(self);
|
2017-07-05 10:28:15 +00:00
|
|
|
if (len != 1)
|
2018-07-11 23:49:00 +00:00
|
|
|
revert();
|
2018-05-08 12:26:01 +00:00
|
|
|
uint8 temp;
|
2017-07-05 10:28:15 +00:00
|
|
|
assembly {
|
|
|
|
temp := byte(0, mload(rStartPos))
|
|
|
|
}
|
|
|
|
return byte(temp);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// @dev Decode an RLPItem into an int. This will not work if the
|
|
|
|
/// RLPItem is a list.
|
|
|
|
/// @param self The RLPItem.
|
|
|
|
/// @return The decoded string.
|
2018-07-02 09:14:28 +00:00
|
|
|
function toInt(RLPItem memory self) internal view returns (int data) {
|
2017-07-05 10:28:15 +00:00
|
|
|
return int(toUint(self));
|
|
|
|
}
|
|
|
|
|
|
|
|
/// @dev Decode an RLPItem into a bytes32. This will not work if the
|
|
|
|
/// RLPItem is a list.
|
|
|
|
/// @param self The RLPItem.
|
|
|
|
/// @return The decoded string.
|
2018-07-02 09:14:28 +00:00
|
|
|
function toBytes32(RLPItem memory self) internal view returns (bytes32 data) {
|
2017-07-05 10:28:15 +00:00
|
|
|
return bytes32(toUint(self));
|
|
|
|
}
|
|
|
|
|
|
|
|
/// @dev Decode an RLPItem into an address. This will not work if the
|
|
|
|
/// RLPItem is a list.
|
|
|
|
/// @param self The RLPItem.
|
|
|
|
/// @return The decoded string.
|
2018-07-02 09:14:28 +00:00
|
|
|
function toAddress(RLPItem memory self) internal view returns (address data) {
|
2017-07-05 10:28:15 +00:00
|
|
|
if(!isData(self))
|
2018-07-11 23:49:00 +00:00
|
|
|
revert();
|
2018-06-21 11:58:38 +00:00
|
|
|
(uint rStartPos, uint len) = _decode(self);
|
2017-07-05 10:28:15 +00:00
|
|
|
if (len != 20)
|
2018-07-11 23:49:00 +00:00
|
|
|
revert();
|
2017-07-05 10:28:15 +00:00
|
|
|
assembly {
|
|
|
|
data := div(mload(rStartPos), exp(256, 12))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the payload offset.
|
2018-07-02 09:14:28 +00:00
|
|
|
function _payloadOffset(RLPItem memory self) private view returns (uint) {
|
2017-07-05 10:28:15 +00:00
|
|
|
if(self._unsafe_length == 0)
|
|
|
|
return 0;
|
|
|
|
uint b0;
|
|
|
|
uint memPtr = self._unsafe_memPtr;
|
|
|
|
assembly {
|
|
|
|
b0 := byte(0, mload(memPtr))
|
|
|
|
}
|
|
|
|
if(b0 < DATA_SHORT_START)
|
|
|
|
return 0;
|
|
|
|
if(b0 < DATA_LONG_START || (b0 >= LIST_SHORT_START && b0 < LIST_LONG_START))
|
|
|
|
return 1;
|
|
|
|
if(b0 < LIST_SHORT_START)
|
|
|
|
return b0 - DATA_LONG_OFFSET + 1;
|
|
|
|
return b0 - LIST_LONG_OFFSET + 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the full length of an RLP item.
|
2018-07-02 09:14:28 +00:00
|
|
|
function _itemLength(uint memPtr) private view returns (uint len) {
|
2017-07-05 10:28:15 +00:00
|
|
|
uint b0;
|
|
|
|
assembly {
|
|
|
|
b0 := byte(0, mload(memPtr))
|
|
|
|
}
|
|
|
|
if (b0 < DATA_SHORT_START)
|
|
|
|
len = 1;
|
|
|
|
else if (b0 < DATA_LONG_START)
|
|
|
|
len = b0 - DATA_SHORT_START + 1;
|
|
|
|
else if (b0 < LIST_SHORT_START) {
|
|
|
|
assembly {
|
|
|
|
let bLen := sub(b0, 0xB7) // bytes length (DATA_LONG_OFFSET)
|
|
|
|
let dLen := div(mload(add(memPtr, 1)), exp(256, sub(32, bLen))) // data length
|
|
|
|
len := add(1, add(bLen, dLen)) // total length
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (b0 < LIST_LONG_START)
|
|
|
|
len = b0 - LIST_SHORT_START + 1;
|
|
|
|
else {
|
|
|
|
assembly {
|
|
|
|
let bLen := sub(b0, 0xF7) // bytes length (LIST_LONG_OFFSET)
|
|
|
|
let dLen := div(mload(add(memPtr, 1)), exp(256, sub(32, bLen))) // data length
|
|
|
|
len := add(1, add(bLen, dLen)) // total length
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get start position and length of the data.
|
2018-07-02 09:14:28 +00:00
|
|
|
function _decode(RLPItem memory self) private view returns (uint memPtr, uint len) {
|
2017-07-05 10:28:15 +00:00
|
|
|
if(!isData(self))
|
2018-07-11 23:49:00 +00:00
|
|
|
revert();
|
2017-07-05 10:28:15 +00:00
|
|
|
uint b0;
|
|
|
|
uint start = self._unsafe_memPtr;
|
|
|
|
assembly {
|
|
|
|
b0 := byte(0, mload(start))
|
|
|
|
}
|
|
|
|
if (b0 < DATA_SHORT_START) {
|
|
|
|
memPtr = start;
|
|
|
|
len = 1;
|
|
|
|
}
|
2018-08-07 18:47:52 +00:00
|
|
|
else if (b0 < DATA_LONG_START) {
|
2017-07-05 10:28:15 +00:00
|
|
|
len = self._unsafe_length - 1;
|
|
|
|
memPtr = start + 1;
|
|
|
|
} else {
|
|
|
|
uint bLen;
|
|
|
|
assembly {
|
|
|
|
bLen := sub(b0, 0xB7) // DATA_LONG_OFFSET
|
|
|
|
}
|
|
|
|
len = self._unsafe_length - 1 - bLen;
|
|
|
|
memPtr = start + bLen + 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Assumes that enough memory has been allocated to store in target.
|
2018-07-02 09:14:28 +00:00
|
|
|
function _copyToBytes(uint btsPtr, bytes memory tgt, uint btsLen) private {
|
2017-07-05 10:28:15 +00:00
|
|
|
// Exploiting the fact that 'tgt' was the last thing to be allocated,
|
|
|
|
// we can write entire words, and just overwrite any excess.
|
|
|
|
assembly {
|
|
|
|
{
|
|
|
|
let words := div(add(btsLen, 31), 32)
|
|
|
|
let rOffset := btsPtr
|
|
|
|
let wOffset := add(tgt, 0x20)
|
2018-07-09 13:04:27 +00:00
|
|
|
|
|
|
|
// Start at arr + 0x20
|
|
|
|
for { let i := 0 } not(eq(i, words)) { i := add(i, 1) }
|
2017-07-05 10:28:15 +00:00
|
|
|
{
|
|
|
|
let offset := mul(i, 0x20)
|
|
|
|
mstore(add(wOffset, offset), mload(add(rOffset, offset)))
|
|
|
|
}
|
|
|
|
mstore(add(tgt, add(0x20, mload(tgt))), 0)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check that an RLP item is valid.
|
2018-07-02 09:14:28 +00:00
|
|
|
function _validate(RLPItem memory self) private view returns (bool ret) {
|
2017-07-05 10:28:15 +00:00
|
|
|
// Check that RLP is well-formed.
|
|
|
|
uint b0;
|
|
|
|
uint b1;
|
|
|
|
uint memPtr = self._unsafe_memPtr;
|
|
|
|
assembly {
|
|
|
|
b0 := byte(0, mload(memPtr))
|
|
|
|
b1 := byte(1, mload(memPtr))
|
|
|
|
}
|
|
|
|
if(b0 == DATA_SHORT_START + 1 && b1 < DATA_SHORT_START)
|
|
|
|
return false;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|