Cleanup some Json::Value methods:

* Rename isArray to isArrayOrNull
* Rename isObject to isObjectOrNull
* Introduce isArray and isObject
* Change as many uses of isArrayorNull to isArray as possible
* Change as many uses of isObjectorNull to isObject as possible
* Reject null JSON arrays for subscribe and unsubscribe
This commit is contained in:
Howard Hinnant
2018-02-01 15:23:41 -05:00
committed by Mike Ellery
parent 20defb4844
commit 1a245234f1
24 changed files with 90 additions and 63 deletions

View File

@@ -1584,7 +1584,7 @@ ApplicationImp::loadLedgerFromFile (
ledger = ledger.get()["accountState"]; ledger = ledger.get()["accountState"];
} }
if (!ledger.get().isArray ()) if (!ledger.get().isArrayOrNull ())
{ {
JLOG(m_journal.fatal()) JLOG(m_journal.fatal())
<< "State nodes must be an array"; << "State nodes must be an array";
@@ -1599,7 +1599,7 @@ ApplicationImp::loadLedgerFromFile (
{ {
Json::Value& entry = ledger.get()[index]; Json::Value& entry = ledger.get()[index];
if (!entry.isObject()) if (!entry.isObjectOrNull())
{ {
JLOG(m_journal.fatal()) JLOG(m_journal.fatal())
<< "Invalid entry in ledger"; << "Invalid entry in ledger";

View File

@@ -79,7 +79,7 @@ accountTxPage (
bool bAdmin, bool bAdmin,
std::uint32_t page_length) std::uint32_t page_length)
{ {
bool lookingForMarker = !token.isNull() && token.isObject(); bool lookingForMarker = token.isObject();
std::uint32_t numberOfResults; std::uint32_t numberOfResults;

View File

@@ -223,7 +223,7 @@ ValidatorList::applyList (
std::vector<std::string> manifests; std::vector<std::string> manifests;
for (auto const& val : newList) for (auto const& val : newList)
{ {
if (val.isObject () && if (val.isObject() &&
val.isMember ("validation_public_key") && val.isMember ("validation_public_key") &&
val["validation_public_key"].isString ()) val["validation_public_key"].isString ())
{ {

View File

@@ -223,7 +223,7 @@ ValidatorSite::onSiteFetch(
Json::Reader r; Json::Reader r;
Json::Value body; Json::Value body;
if (r.parse(res.body.data(), body) && if (r.parse(res.body.data(), body) &&
body.isObject () && body.isObject() &&
body.isMember("blob") && body["blob"].isString () && body.isMember("blob") && body["blob"].isString () &&
body.isMember("manifest") && body["manifest"].isString () && body.isMember("manifest") && body["manifest"].isString () &&
body.isMember("signature") && body["signature"].isString() && body.isMember("signature") && body["signature"].isString() &&

View File

@@ -196,7 +196,7 @@ namespace {
template <class Object> template <class Object>
void doCopyFrom (Object& to, Json::Value const& from) void doCopyFrom (Object& to, Json::Value const& from)
{ {
assert (from.isObject()); assert (from.isObjectOrNull());
auto members = from.getMemberNames(); auto members = from.getMemberNames();
for (auto& m: members) for (auto& m: members)
to[m] = from[m]; to[m] = from[m];

View File

@@ -124,7 +124,7 @@ Reader::parse ( const char* beginDoc, const char* endDoc,
Token token; Token token;
skipCommentTokens ( token ); skipCommentTokens ( token );
if ( !root.isArray () && !root.isObject () ) if ( !root.isNull() && !root.isArray() && !root.isObject() )
{ {
// Set error location to start of doc, ideally should be first token found in doc // Set error location to start of doc, ideally should be first token found in doc
token.type_ = tokenError; token.type_ = tokenError;

View File

@@ -779,10 +779,10 @@ Value::operator bool () const
if (isString ()) if (isString ())
{ {
auto s = asCString(); auto s = asCString();
return s && strlen(s); return s && s[0];
} }
return ! (isArray () || isObject ()) || size (); return ! (isArray() || isObject()) || size ();
} }
void void
@@ -1092,14 +1092,26 @@ Value::isString () const
bool bool
Value::isArray () const Value::isArray() const
{
return type_ == arrayValue;
}
bool
Value::isArrayOrNull () const
{ {
return type_ == nullValue || type_ == arrayValue; return type_ == nullValue || type_ == arrayValue;
} }
bool bool
Value::isObject () const Value::isObject() const
{
return type_ == objectValue;
}
bool
Value::isObjectOrNull () const
{ {
return type_ == nullValue || type_ == objectValue; return type_ == nullValue || type_ == objectValue;
} }

View File

@@ -426,7 +426,7 @@ StyledWriter::isMultineArray ( const Value& value )
{ {
const Value& childValue = value[index]; const Value& childValue = value[index];
isMultiLine = isMultiLine || isMultiLine = isMultiLine ||
( (childValue.isArray () || childValue.isObject ()) && ( (childValue.isArray() || childValue.isObject()) &&
childValue.size () > 0 ); childValue.size () > 0 );
} }
@@ -660,7 +660,7 @@ StyledStreamWriter::isMultineArray ( const Value& value )
{ {
const Value& childValue = value[index]; const Value& childValue = value[index];
isMultiLine = isMultiLine || isMultiLine = isMultiLine ||
( (childValue.isArray () || childValue.isObject ()) && ( (childValue.isArray() || childValue.isObject()) &&
childValue.size () > 0 ); childValue.size () > 0 );
} }

View File

@@ -262,8 +262,10 @@ public:
bool isDouble () const; bool isDouble () const;
bool isNumeric () const; bool isNumeric () const;
bool isString () const; bool isString () const;
bool isArray () const; bool isArray() const;
bool isObject () const; bool isArrayOrNull () const;
bool isObject() const;
bool isObjectOrNull () const;
bool isConvertibleTo ( ValueType other ) const; bool isConvertibleTo ( ValueType other ) const;

View File

@@ -173,7 +173,7 @@ private:
{ {
Json::Value v (Json::objectValue); Json::Value v (Json::objectValue);
if (jvParams.isArray () && (jvParams.size () > 0)) if (jvParams.isArray() && (jvParams.size () > 0))
v[jss::params] = jvParams; v[jss::params] = jvParams;
return v; return v;
@@ -513,7 +513,7 @@ private:
if (reader.parse (jvParams[1u].asString (), jvRequest)) if (reader.parse (jvParams[1u].asString (), jvRequest))
{ {
if (!jvRequest.isObject ()) if (!jvRequest.isObjectOrNull ())
return rpcError (rpcINVALID_PARAMS); return rpcError (rpcINVALID_PARAMS);
jvRequest[jss::method] = jvParams[0u]; jvRequest[jss::method] = jvParams[0u];
@@ -544,7 +544,8 @@ private:
jv.isMember(jss::id) && jv.isMember(jss::method)) jv.isMember(jss::id) && jv.isMember(jss::method))
{ {
if (jv.isMember(jss::params) && if (jv.isMember(jss::params) &&
!(jv[jss::params].isArray() || jv[jss::params].isObject())) !(jv[jss::params].isNull() || jv[jss::params].isArray() ||
jv[jss::params].isObject()))
return false; return false;
return true; return true;
} }

View File

@@ -35,7 +35,7 @@ Json::Value rpcError (int iError, Json::Value jvResult)
// VFALCO NOTE Deprecated function // VFALCO NOTE Deprecated function
bool isRpcError (Json::Value jvResult) bool isRpcError (Json::Value jvResult)
{ {
return jvResult.isObject () && jvResult.isMember (jss::error); return jvResult.isObject() && jvResult.isMember (jss::error);
} }
} // ripple } // ripple

View File

@@ -806,13 +806,17 @@ amountFromJson (SField const& name, Json::Value const& v)
Json::Value currency; Json::Value currency;
Json::Value issuer; Json::Value issuer;
if (v.isObject ()) if (v.isNull())
{
Throw<std::runtime_error> ("XRP may not be specified with a null Json value");
}
else if (v.isObject())
{ {
value = v[jss::value]; value = v[jss::value];
currency = v[jss::currency]; currency = v[jss::currency];
issuer = v[jss::issuer]; issuer = v[jss::issuer];
} }
else if (v.isArray ()) else if (v.isArray())
{ {
value = v.get (Json::UInt (0), 0); value = v.get (Json::UInt (0), 0);
currency = v.get (Json::UInt (1), Json::nullValue); currency = v.get (Json::UInt (1), Json::nullValue);
@@ -846,7 +850,7 @@ amountFromJson (SField const& name, Json::Value const& v)
if (native) if (native)
{ {
if (v.isObject ()) if (v.isObjectOrNull ())
Throw<std::runtime_error> ("XRP may not be specified as an object"); Throw<std::runtime_error> ("XRP may not be specified as an object");
issue = xrpIssue (); issue = xrpIssue ();
} }

View File

@@ -495,7 +495,7 @@ static boost::optional<detail::STVar> parseLeaf (
break; break;
case STI_VECTOR256: case STI_VECTOR256:
if (! value.isArray ()) if (! value.isArrayOrNull ())
{ {
error = array_expected (json_name, fieldName); error = array_expected (json_name, fieldName);
return ret; return ret;
@@ -521,7 +521,7 @@ static boost::optional<detail::STVar> parseLeaf (
break; break;
case STI_PATHSET: case STI_PATHSET:
if (!value.isArray ()) if (!value.isArrayOrNull ())
{ {
error = array_expected (json_name, fieldName); error = array_expected (json_name, fieldName);
return ret; return ret;
@@ -535,7 +535,7 @@ static boost::optional<detail::STVar> parseLeaf (
{ {
STPath p; STPath p;
if (!value[i].isArray ()) if (!value[i].isArrayOrNull ())
{ {
std::stringstream ss; std::stringstream ss;
ss << fieldName << "[" << i << "]"; ss << fieldName << "[" << i << "]";
@@ -555,7 +555,7 @@ static boost::optional<detail::STVar> parseLeaf (
Json::Value pathEl = value[i][j]; Json::Value pathEl = value[i][j];
if (!pathEl.isObject ()) if (!pathEl.isObject())
{ {
error = not_an_object (element_name); error = not_an_object (element_name);
return ret; return ret;
@@ -709,7 +709,7 @@ static boost::optional <STObject> parseObject (
int depth, int depth,
Json::Value& error) Json::Value& error)
{ {
if (! json.isObject ()) if (! json.isObjectOrNull ())
{ {
error = not_an_object (json_name); error = not_an_object (json_name);
return boost::none; return boost::none;
@@ -743,7 +743,7 @@ static boost::optional <STObject> parseObject (
case STI_TRANSACTION: case STI_TRANSACTION:
case STI_LEDGERENTRY: case STI_LEDGERENTRY:
case STI_VALIDATION: case STI_VALIDATION:
if (! value.isObject ()) if (! value.isObjectOrNull ())
{ {
error = not_an_object (json_name, fieldName); error = not_an_object (json_name, fieldName);
return boost::none; return boost::none;
@@ -816,7 +816,7 @@ static boost::optional <detail::STVar> parseArray (
int depth, int depth,
Json::Value& error) Json::Value& error)
{ {
if (! json.isArray ()) if (! json.isArrayOrNull ())
{ {
error = not_an_array (json_name); error = not_an_array (json_name);
return boost::none; return boost::none;
@@ -834,11 +834,12 @@ static boost::optional <detail::STVar> parseArray (
for (Json::UInt i = 0; json.isValidIndex (i); ++i) for (Json::UInt i = 0; json.isValidIndex (i); ++i)
{ {
bool const isObject (json[i].isObject()); bool const isObjectOrNull (json[i].isObjectOrNull());
bool const singleKey (isObject ? json[i].size() == 1 : true); bool const singleKey (isObjectOrNull ? json[i].size() == 1 : true);
if (!isObject || !singleKey) if (!isObjectOrNull || !singleKey)
{ {
// null values are !singleKey
error = singleton_expected (json_name, i); error = singleton_expected (json_name, i);
return boost::none; return boost::none;
} }

View File

@@ -52,22 +52,21 @@ Json::Value doBookOffers (RPC::Context& context)
if (!context.params.isMember (jss::taker_gets)) if (!context.params.isMember (jss::taker_gets))
return RPC::missing_field_error (jss::taker_gets); return RPC::missing_field_error (jss::taker_gets);
if (!context.params[jss::taker_pays].isObject ()) Json::Value const& taker_pays = context.params[jss::taker_pays];
Json::Value const& taker_gets = context.params[jss::taker_gets];
if (!taker_pays.isObjectOrNull ())
return RPC::object_field_error (jss::taker_pays); return RPC::object_field_error (jss::taker_pays);
if (!context.params[jss::taker_gets].isObject ()) if (!taker_gets.isObjectOrNull ())
return RPC::object_field_error (jss::taker_gets); return RPC::object_field_error (jss::taker_gets);
Json::Value const& taker_pays (context.params[jss::taker_pays]);
if (!taker_pays.isMember (jss::currency)) if (!taker_pays.isMember (jss::currency))
return RPC::missing_field_error ("taker_pays.currency"); return RPC::missing_field_error ("taker_pays.currency");
if (! taker_pays [jss::currency].isString ()) if (! taker_pays [jss::currency].isString ())
return RPC::expected_field_error ("taker_pays.currency", "string"); return RPC::expected_field_error ("taker_pays.currency", "string");
Json::Value const& taker_gets = context.params[jss::taker_gets];
if (! taker_gets.isMember (jss::currency)) if (! taker_gets.isMember (jss::currency))
return RPC::missing_field_error ("taker_gets.currency"); return RPC::missing_field_error ("taker_gets.currency");

View File

@@ -116,7 +116,8 @@ Json::Value doGatewayBalances (RPC::Context& context)
Json::Value const& hw = params[jss::hotwallet]; Json::Value const& hw = params[jss::hotwallet];
bool valid = true; bool valid = true;
if (hw.isArray()) // null is treated as a valid 0-sized array of hotwallet
if (hw.isArrayOrNull())
{ {
for (unsigned i = 0; i < hw.size(); ++i) for (unsigned i = 0; i < hw.size(); ++i)
valid &= addHotWallet (hw[i]); valid &= addHotWallet (hw[i]);

View File

@@ -64,7 +64,11 @@ Json::Value doLedgerEntry (RPC::Context& context)
} }
else if (context.params.isMember (jss::directory)) else if (context.params.isMember (jss::directory))
{ {
if (!context.params[jss::directory].isObject ()) if (context.params[jss::directory].isNull())
{
jvResult[jss::error] = "malformedRequest";
}
else if (!context.params[jss::directory].isObject())
{ {
uNodeIndex.SetHex (context.params[jss::directory].asString ()); uNodeIndex.SetHex (context.params[jss::directory].asString ());
} }
@@ -114,7 +118,7 @@ Json::Value doLedgerEntry (RPC::Context& context)
} }
else if (context.params.isMember (jss::offer)) else if (context.params.isMember (jss::offer))
{ {
if (!context.params[jss::offer].isObject ()) if (!context.params[jss::offer].isObject())
{ {
uNodeIndex.SetHex (context.params[jss::offer].asString ()); uNodeIndex.SetHex (context.params[jss::offer].asString ());
} }
@@ -140,10 +144,10 @@ Json::Value doLedgerEntry (RPC::Context& context)
Currency uCurrency; Currency uCurrency;
Json::Value jvRippleState = context.params[jss::ripple_state]; Json::Value jvRippleState = context.params[jss::ripple_state];
if (!jvRippleState.isObject () if (!jvRippleState.isObject()
|| !jvRippleState.isMember (jss::currency) || !jvRippleState.isMember (jss::currency)
|| !jvRippleState.isMember (jss::accounts) || !jvRippleState.isMember (jss::accounts)
|| !jvRippleState[jss::accounts].isArray () || !jvRippleState[jss::accounts].isArray()
|| 2 != jvRippleState[jss::accounts].size () || 2 != jvRippleState[jss::accounts].size ()
|| !jvRippleState[jss::accounts][0u].isString () || !jvRippleState[jss::accounts][0u].isString ()
|| !jvRippleState[jss::accounts][1u].isString () || !jvRippleState[jss::accounts][1u].isString ()

View File

@@ -192,11 +192,11 @@ Json::Value doSubscribe (RPC::Context& context)
for (auto& j: context.params[jss::books]) for (auto& j: context.params[jss::books])
{ {
if (!j.isObject () if (!j.isObject()
|| !j.isMember (jss::taker_pays) || !j.isMember (jss::taker_pays)
|| !j.isMember (jss::taker_gets) || !j.isMember (jss::taker_gets)
|| !j[jss::taker_pays].isObject () || !j[jss::taker_pays].isObjectOrNull ()
|| !j[jss::taker_gets].isObject ()) || !j[jss::taker_gets].isObjectOrNull ())
return rpcError (rpcINVALID_PARAMS); return rpcError (rpcINVALID_PARAMS);
Book book; Book book;

View File

@@ -60,7 +60,7 @@ Json::Value doUnsubscribe (RPC::Context& context)
if (context.params.isMember (jss::streams)) if (context.params.isMember (jss::streams))
{ {
if (! context.params[jss::streams].isArray ()) if (! context.params[jss::streams].isArray())
return rpcError (rpcINVALID_PARAMS); return rpcError (rpcINVALID_PARAMS);
for (auto& it: context.params[jss::streams]) for (auto& it: context.params[jss::streams])
@@ -139,8 +139,8 @@ Json::Value doUnsubscribe (RPC::Context& context)
if (! jv.isObject() || if (! jv.isObject() ||
! jv.isMember(jss::taker_pays) || ! jv.isMember(jss::taker_pays) ||
! jv.isMember(jss::taker_gets) || ! jv.isMember(jss::taker_gets) ||
! jv[jss::taker_pays].isObject() || ! jv[jss::taker_pays].isObjectOrNull() ||
! jv[jss::taker_gets].isObject()) ! jv[jss::taker_gets].isObjectOrNull())
{ {
return rpcError(rpcINVALID_PARAMS); return rpcError(rpcINVALID_PARAMS);
} }

View File

@@ -334,7 +334,6 @@ ServerHandlerImp::onWSMessage(
auto const size = boost::asio::buffer_size(buffers); auto const size = boost::asio::buffer_size(buffers);
if (size > RPC::Tuning::maxRequestSize || if (size > RPC::Tuning::maxRequestSize ||
! Json::Reader{}.parse(jv, buffers) || ! Json::Reader{}.parse(jv, buffers) ||
! jv ||
! jv.isObject()) ! jv.isObject())
{ {
Json::Value jvResult(Json::objectValue); Json::Value jvResult(Json::objectValue);
@@ -603,7 +602,7 @@ ServerHandlerImp::processRequest (Port const& port,
if (jsonRPC.isMember(jss::params) && if (jsonRPC.isMember(jss::params) &&
jsonRPC[jss::params].isArray() && jsonRPC[jss::params].isArray() &&
jsonRPC[jss::params].size() > 0 && jsonRPC[jss::params].size() > 0 &&
jsonRPC[jss::params][Json::UInt(0)].isObject()) jsonRPC[jss::params][Json::UInt(0)].isObjectOrNull())
{ {
role = requestRole( role = requestRole(
required, required,
@@ -712,7 +711,7 @@ ServerHandlerImp::processRequest (Port const& port,
if (! params) if (! params)
params = Json::Value (Json::objectValue); params = Json::Value (Json::objectValue);
else if (!params.isArray () || params.size() != 1) else if (!params.isArray() || params.size() != 1)
{ {
usage.charge(Resource::feeInvalidRPC); usage.charge(Resource::feeInvalidRPC);
HTTPReply (400, "params unparseable", output, rpcJ); HTTPReply (400, "params unparseable", output, rpcJ);
@@ -721,7 +720,7 @@ ServerHandlerImp::processRequest (Port const& port,
else else
{ {
params = std::move (params[0u]); params = std::move (params[0u]);
if (!params.isObject()) if (!params.isObjectOrNull())
{ {
usage.charge(Resource::feeInvalidRPC); usage.charge(Resource::feeInvalidRPC);
HTTPReply (400, "params unparseable", output, rpcJ); HTTPReply (400, "params unparseable", output, rpcJ);

View File

@@ -253,7 +253,7 @@ checkTxJsonFields (
{ {
std::pair<Json::Value, AccountID> ret; std::pair<Json::Value, AccountID> ret;
if (! tx_json.isObject ()) if (!tx_json.isObject())
{ {
ret.first = RPC::object_field_error (jss::tx_json); ret.first = RPC::object_field_error (jss::tx_json);
return ret; return ret;

View File

@@ -213,8 +213,7 @@ struct Regression_test : public beast::unit_test::suite
std::vector<boost::asio::const_buffer> buffers; std::vector<boost::asio::const_buffer> buffers;
buffers.emplace_back(buffer(request, 1024)); buffers.emplace_back(buffer(request, 1024));
buffers.emplace_back(buffer(request.data() + 1024, request.length() - 1024)); buffers.emplace_back(buffer(request.data() + 1024, request.length() - 1024));
BEAST_EXPECT(jrReader.parse(jvRequest, buffers) && BEAST_EXPECT(jrReader.parse(jvRequest, buffers) && jvRequest.isObject());
jvRequest && jvRequest.isObject());
} }
void run() override void run() override

View File

@@ -38,9 +38,7 @@ public:
bool parseJSONString (std::string const& json, Json::Value& to) bool parseJSONString (std::string const& json, Json::Value& to)
{ {
Json::Reader reader; Json::Reader reader;
return reader.parse(json, to) && return reader.parse(json, to) && to.isObject();
bool (to) &&
to.isObject();
} }
void testParseJSONArrayWithInvalidChildrenObjects () void testParseJSONArrayWithInvalidChildrenObjects ()

View File

@@ -64,7 +64,7 @@ class AmendmentBlocked_test : public beast::unit_test::suite
pf_req[jss::destination_amount] = bob["USD"](20).value ().getJson (0); pf_req[jss::destination_amount] = bob["USD"](20).value ().getJson (0);
jr = wsc->invoke("path_find", pf_req) [jss::result]; jr = wsc->invoke("path_find", pf_req) [jss::result];
BEAST_EXPECT (jr.isMember (jss::alternatives) && BEAST_EXPECT (jr.isMember (jss::alternatives) &&
jr[jss::alternatives].isArray () && jr[jss::alternatives].isArray() &&
jr[jss::alternatives].size () == 1); jr[jss::alternatives].size () == 1);
// submit // submit

View File

@@ -462,11 +462,16 @@ public:
BEAST_EXPECT(jr[jss::error_message] == "You don't have permission for this command."); BEAST_EXPECT(jr[jss::error_message] == "You don't have permission for this command.");
} }
std::initializer_list<Json::Value> const nonArrays {Json::nullValue,
Json::intValue, Json::uintValue, Json::realValue, "",
Json::booleanValue, Json::objectValue};
for (auto const& f : {jss::accounts_proposed, jss::accounts}) for (auto const& f : {jss::accounts_proposed, jss::accounts})
{ {
for (auto const& nonArray : nonArrays)
{ {
Json::Value jv; Json::Value jv;
jv[f] = ""; jv[f] = nonArray;
auto jr = wsc->invoke(method, jv) [jss::result]; auto jr = wsc->invoke(method, jv) [jss::result];
BEAST_EXPECT(jr[jss::error] == "invalidParams"); BEAST_EXPECT(jr[jss::error] == "invalidParams");
BEAST_EXPECT(jr[jss::error_message] == "Invalid parameters."); BEAST_EXPECT(jr[jss::error_message] == "Invalid parameters.");
@@ -481,9 +486,10 @@ public:
} }
} }
for (auto const& nonArray : nonArrays)
{ {
Json::Value jv; Json::Value jv;
jv[jss::books] = ""; jv[jss::books] = nonArray;
auto jr = wsc->invoke(method, jv) [jss::result]; auto jr = wsc->invoke(method, jv) [jss::result];
BEAST_EXPECT(jr[jss::error] == "invalidParams"); BEAST_EXPECT(jr[jss::error] == "invalidParams");
BEAST_EXPECT(jr[jss::error_message] == "Invalid parameters."); BEAST_EXPECT(jr[jss::error_message] == "Invalid parameters.");
@@ -608,9 +614,10 @@ public:
BEAST_EXPECT(jr[jss::error_message] == "No such market."); BEAST_EXPECT(jr[jss::error_message] == "No such market.");
} }
for (auto const& nonArray : nonArrays)
{ {
Json::Value jv; Json::Value jv;
jv[jss::streams] = ""; jv[jss::streams] = nonArray;
auto jr = wsc->invoke(method, jv) [jss::result]; auto jr = wsc->invoke(method, jv) [jss::result];
BEAST_EXPECT(jr[jss::error] == "invalidParams"); BEAST_EXPECT(jr[jss::error] == "invalidParams");
BEAST_EXPECT(jr[jss::error_message] == "Invalid parameters."); BEAST_EXPECT(jr[jss::error_message] == "Invalid parameters.");