mirror of
https://github.com/NixOS/nix.git
synced 2025-11-09 03:56:01 +01:00
Use less c_str() in the evaluator, and other cleanups
It is better to avoid null termination for performance and memory safety, wherever possible. These are good cleanups extracted from the Pascal String work that we can land by themselves first, shrinking the diff in that PR. Co-Authored-By: Aspen Smith <root@gws.fyi> Co-Authored-By: Sergei Zimmerman <sergei@zimmerman.foo>
This commit is contained in:
parent
2d83bc6b83
commit
bd42092873
16 changed files with 64 additions and 38 deletions
|
|
@ -235,7 +235,7 @@ nix_get_string(nix_c_context * context, const nix_value * value, nix_get_string_
|
||||||
try {
|
try {
|
||||||
auto & v = check_value_in(value);
|
auto & v = check_value_in(value);
|
||||||
assert(v.type() == nix::nString);
|
assert(v.type() == nix::nString);
|
||||||
call_nix_get_string_callback(v.c_str(), callback, user_data);
|
call_nix_get_string_callback(v.string_view(), callback, user_data);
|
||||||
}
|
}
|
||||||
NIXC_CATCH_ERRS
|
NIXC_CATCH_ERRS
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@ MATCHER_P(IsStringEq, s, fmt("The string is equal to \"%1%\"", s))
|
||||||
if (arg.type() != nString) {
|
if (arg.type() != nString) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return std::string_view(arg.c_str()) == s;
|
return arg.string_view() == s;
|
||||||
}
|
}
|
||||||
|
|
||||||
MATCHER_P(IsIntEq, v, fmt("The string is equal to \"%1%\"", v))
|
MATCHER_P(IsIntEq, v, fmt("The string is equal to \"%1%\"", v))
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#include "nix/expr/value.hh"
|
#include "nix/expr/value.hh"
|
||||||
|
|
||||||
#include "nix/store/tests/libstore.hh"
|
#include "nix/store/tests/libstore.hh"
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
namespace nix {
|
namespace nix {
|
||||||
|
|
||||||
|
|
@ -22,4 +23,21 @@ TEST_F(ValueTest, vInt)
|
||||||
ASSERT_EQ(true, vInt.isValid());
|
ASSERT_EQ(true, vInt.isValid());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(ValueTest, staticString)
|
||||||
|
{
|
||||||
|
Value vStr1;
|
||||||
|
Value vStr2;
|
||||||
|
vStr1.mkStringNoCopy("foo");
|
||||||
|
vStr2.mkStringNoCopy("foo");
|
||||||
|
|
||||||
|
auto sd1 = vStr1.string_view();
|
||||||
|
auto sd2 = vStr2.string_view();
|
||||||
|
|
||||||
|
// The strings should be the same
|
||||||
|
ASSERT_EQ(sd1, sd2);
|
||||||
|
|
||||||
|
// The strings should also be backed by the same (static) allocation
|
||||||
|
ASSERT_EQ(sd1.data(), sd2.data());
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace nix
|
} // namespace nix
|
||||||
|
|
|
||||||
|
|
@ -406,7 +406,7 @@ Value & AttrCursor::forceValue()
|
||||||
|
|
||||||
if (root->db && (!cachedValue || std::get_if<placeholder_t>(&cachedValue->second))) {
|
if (root->db && (!cachedValue || std::get_if<placeholder_t>(&cachedValue->second))) {
|
||||||
if (v.type() == nString)
|
if (v.type() == nString)
|
||||||
cachedValue = {root->db->setString(getKey(), v.c_str(), v.context()), string_t{v.c_str(), {}}};
|
cachedValue = {root->db->setString(getKey(), v.string_view(), v.context()), string_t{v.string_view(), {}}};
|
||||||
else if (v.type() == nPath) {
|
else if (v.type() == nPath) {
|
||||||
auto path = v.path().path;
|
auto path = v.path().path;
|
||||||
cachedValue = {root->db->setString(getKey(), path.abs()), string_t{path.abs(), {}}};
|
cachedValue = {root->db->setString(getKey(), path.abs()), string_t{path.abs(), {}}};
|
||||||
|
|
@ -541,7 +541,7 @@ std::string AttrCursor::getString()
|
||||||
if (v.type() != nString && v.type() != nPath)
|
if (v.type() != nString && v.type() != nPath)
|
||||||
root->state.error<TypeError>("'%s' is not a string but %s", getAttrPathStr(), showType(v)).debugThrow();
|
root->state.error<TypeError>("'%s' is not a string but %s", getAttrPathStr(), showType(v)).debugThrow();
|
||||||
|
|
||||||
return v.type() == nString ? v.c_str() : v.path().to_string();
|
return v.type() == nString ? std::string(v.string_view()) : v.path().to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
string_t AttrCursor::getStringWithContext()
|
string_t AttrCursor::getStringWithContext()
|
||||||
|
|
@ -580,7 +580,7 @@ string_t AttrCursor::getStringWithContext()
|
||||||
if (v.type() == nString) {
|
if (v.type() == nString) {
|
||||||
NixStringContext context;
|
NixStringContext context;
|
||||||
copyContext(v, context);
|
copyContext(v, context);
|
||||||
return {v.c_str(), std::move(context)};
|
return {std::string{v.string_view()}, std::move(context)};
|
||||||
} else if (v.type() == nPath)
|
} else if (v.type() == nPath)
|
||||||
return {v.path().to_string(), {}};
|
return {v.path().to_string(), {}};
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -2366,12 +2366,15 @@ BackedStringView EvalState::coerceToString(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (v.type() == nPath) {
|
if (v.type() == nPath) {
|
||||||
return !canonicalizePath && !copyToStore
|
if (!canonicalizePath && !copyToStore) {
|
||||||
? // FIXME: hack to preserve path literals that end in a
|
// FIXME: hack to preserve path literals that end in a
|
||||||
// slash, as in /foo/${x}.
|
// slash, as in /foo/${x}.
|
||||||
v.pathStr()
|
return v.pathStrView();
|
||||||
: copyToStore ? store->printStorePath(copyPathToStore(context, v.path()))
|
} else if (copyToStore) {
|
||||||
: std::string(v.path().path.abs());
|
return store->printStorePath(copyPathToStore(context, v.path()));
|
||||||
|
} else {
|
||||||
|
return std::string{v.path().path.abs()};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (v.type() == nAttrs) {
|
if (v.type() == nAttrs) {
|
||||||
|
|
@ -2624,7 +2627,7 @@ void EvalState::assertEqValues(Value & v1, Value & v2, const PosIdx pos, std::st
|
||||||
return;
|
return;
|
||||||
|
|
||||||
case nString:
|
case nString:
|
||||||
if (strcmp(v1.c_str(), v2.c_str()) != 0) {
|
if (v1.string_view() != v2.string_view()) {
|
||||||
error<AssertionError>(
|
error<AssertionError>(
|
||||||
"string '%s' is not equal to string '%s'",
|
"string '%s' is not equal to string '%s'",
|
||||||
ValuePrinter(*this, v1, errorPrintOptions),
|
ValuePrinter(*this, v1, errorPrintOptions),
|
||||||
|
|
@ -2641,7 +2644,7 @@ void EvalState::assertEqValues(Value & v1, Value & v2, const PosIdx pos, std::st
|
||||||
ValuePrinter(*this, v2, errorPrintOptions))
|
ValuePrinter(*this, v2, errorPrintOptions))
|
||||||
.debugThrow();
|
.debugThrow();
|
||||||
}
|
}
|
||||||
if (strcmp(v1.pathStr(), v2.pathStr()) != 0) {
|
if (v1.pathStrView() != v2.pathStrView()) {
|
||||||
error<AssertionError>(
|
error<AssertionError>(
|
||||||
"path '%s' is not equal to path '%s'",
|
"path '%s' is not equal to path '%s'",
|
||||||
ValuePrinter(*this, v1, errorPrintOptions),
|
ValuePrinter(*this, v1, errorPrintOptions),
|
||||||
|
|
@ -2807,12 +2810,12 @@ bool EvalState::eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_v
|
||||||
return v1.boolean() == v2.boolean();
|
return v1.boolean() == v2.boolean();
|
||||||
|
|
||||||
case nString:
|
case nString:
|
||||||
return strcmp(v1.c_str(), v2.c_str()) == 0;
|
return v1.string_view() == v2.string_view();
|
||||||
|
|
||||||
case nPath:
|
case nPath:
|
||||||
return
|
return
|
||||||
// FIXME: compare accessors by their fingerprint.
|
// FIXME: compare accessors by their fingerprint.
|
||||||
v1.pathAccessor() == v2.pathAccessor() && strcmp(v1.pathStr(), v2.pathStr()) == 0;
|
v1.pathAccessor() == v2.pathAccessor() && v1.pathStrView() == v2.pathStrView();
|
||||||
|
|
||||||
case nNull:
|
case nNull:
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,7 @@ PackageInfo::Outputs PackageInfo::queryOutputs(bool withPaths, bool onlyOutputsT
|
||||||
for (auto elem : outTI->listView()) {
|
for (auto elem : outTI->listView()) {
|
||||||
if (elem->type() != nString)
|
if (elem->type() != nString)
|
||||||
throw errMsg;
|
throw errMsg;
|
||||||
auto out = outputs.find(elem->c_str());
|
auto out = outputs.find(elem->string_view());
|
||||||
if (out == outputs.end())
|
if (out == outputs.end())
|
||||||
throw errMsg;
|
throw errMsg;
|
||||||
result.insert(*out);
|
result.insert(*out);
|
||||||
|
|
@ -245,7 +245,7 @@ std::string PackageInfo::queryMetaString(const std::string & name)
|
||||||
Value * v = queryMeta(name);
|
Value * v = queryMeta(name);
|
||||||
if (!v || v->type() != nString)
|
if (!v || v->type() != nString)
|
||||||
return "";
|
return "";
|
||||||
return v->c_str();
|
return std::string{v->string_view()};
|
||||||
}
|
}
|
||||||
|
|
||||||
NixInt PackageInfo::queryMetaInt(const std::string & name, NixInt def)
|
NixInt PackageInfo::queryMetaInt(const std::string & name, NixInt def)
|
||||||
|
|
@ -258,7 +258,7 @@ NixInt PackageInfo::queryMetaInt(const std::string & name, NixInt def)
|
||||||
if (v->type() == nString) {
|
if (v->type() == nString) {
|
||||||
/* Backwards compatibility with before we had support for
|
/* Backwards compatibility with before we had support for
|
||||||
integer meta fields. */
|
integer meta fields. */
|
||||||
if (auto n = string2Int<NixInt::Inner>(v->c_str()))
|
if (auto n = string2Int<NixInt::Inner>(v->string_view()))
|
||||||
return NixInt{*n};
|
return NixInt{*n};
|
||||||
}
|
}
|
||||||
return def;
|
return def;
|
||||||
|
|
@ -274,7 +274,7 @@ NixFloat PackageInfo::queryMetaFloat(const std::string & name, NixFloat def)
|
||||||
if (v->type() == nString) {
|
if (v->type() == nString) {
|
||||||
/* Backwards compatibility with before we had support for
|
/* Backwards compatibility with before we had support for
|
||||||
float meta fields. */
|
float meta fields. */
|
||||||
if (auto n = string2Float<NixFloat>(v->c_str()))
|
if (auto n = string2Float<NixFloat>(v->string_view()))
|
||||||
return *n;
|
return *n;
|
||||||
}
|
}
|
||||||
return def;
|
return def;
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ namespace nix {
|
||||||
struct PackageInfo
|
struct PackageInfo
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
typedef std::map<std::string, std::optional<StorePath>> Outputs;
|
typedef std::map<std::string, std::optional<StorePath>, std::less<>> Outputs;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
EvalState * state;
|
EvalState * state;
|
||||||
|
|
|
||||||
|
|
@ -1109,7 +1109,7 @@ public:
|
||||||
|
|
||||||
std::string_view string_view() const noexcept
|
std::string_view string_view() const noexcept
|
||||||
{
|
{
|
||||||
return std::string_view(getStorage<StringWithContext>().c_str);
|
return std::string_view{getStorage<StringWithContext>().c_str};
|
||||||
}
|
}
|
||||||
|
|
||||||
const char * c_str() const noexcept
|
const char * c_str() const noexcept
|
||||||
|
|
@ -1177,6 +1177,11 @@ public:
|
||||||
return getStorage<Path>().path;
|
return getStorage<Path>().path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string_view pathStrView() const noexcept
|
||||||
|
{
|
||||||
|
return std::string_view{getStorage<Path>().path};
|
||||||
|
}
|
||||||
|
|
||||||
SourceAccessor * pathAccessor() const noexcept
|
SourceAccessor * pathAccessor() const noexcept
|
||||||
{
|
{
|
||||||
return getStorage<Path>().accessor;
|
return getStorage<Path>().accessor;
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ void ExprString::show(const SymbolTable & symbols, std::ostream & str) const
|
||||||
|
|
||||||
void ExprPath::show(const SymbolTable & symbols, std::ostream & str) const
|
void ExprPath::show(const SymbolTable & symbols, std::ostream & str) const
|
||||||
{
|
{
|
||||||
str << v.pathStr();
|
str << v.pathStrView();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ExprVar::show(const SymbolTable & symbols, std::ostream & str) const
|
void ExprVar::show(const SymbolTable & symbols, std::ostream & str) const
|
||||||
|
|
|
||||||
|
|
@ -691,12 +691,12 @@ struct CompareValues
|
||||||
case nFloat:
|
case nFloat:
|
||||||
return v1->fpoint() < v2->fpoint();
|
return v1->fpoint() < v2->fpoint();
|
||||||
case nString:
|
case nString:
|
||||||
return strcmp(v1->c_str(), v2->c_str()) < 0;
|
return v1->string_view() < v2->string_view();
|
||||||
case nPath:
|
case nPath:
|
||||||
// Note: we don't take the accessor into account
|
// Note: we don't take the accessor into account
|
||||||
// since it's not obvious how to compare them in a
|
// since it's not obvious how to compare them in a
|
||||||
// reproducible way.
|
// reproducible way.
|
||||||
return strcmp(v1->pathStr(), v2->pathStr()) < 0;
|
return v1->pathStrView() < v2->pathStrView();
|
||||||
case nList:
|
case nList:
|
||||||
// Lexicographic comparison
|
// Lexicographic comparison
|
||||||
for (size_t i = 0;; i++) {
|
for (size_t i = 0;; i++) {
|
||||||
|
|
@ -2930,7 +2930,7 @@ static void prim_attrNames(EvalState & state, const PosIdx pos, Value ** args, V
|
||||||
for (const auto & [n, i] : enumerate(*args[0]->attrs()))
|
for (const auto & [n, i] : enumerate(*args[0]->attrs()))
|
||||||
list[n] = Value::toPtr(state.symbols[i.name]);
|
list[n] = Value::toPtr(state.symbols[i.name]);
|
||||||
|
|
||||||
std::sort(list.begin(), list.end(), [](Value * v1, Value * v2) { return strcmp(v1->c_str(), v2->c_str()) < 0; });
|
std::sort(list.begin(), list.end(), [](Value * v1, Value * v2) { return v1->string_view() < v2->string_view(); });
|
||||||
|
|
||||||
v.mkList(list);
|
v.mkList(list);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ json printValueAsJSON(
|
||||||
|
|
||||||
case nString:
|
case nString:
|
||||||
copyContext(v, context);
|
copyContext(v, context);
|
||||||
out = v.c_str();
|
out = v.string_view();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case nPath:
|
case nPath:
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ static void printValueAsXML(
|
||||||
case nString:
|
case nString:
|
||||||
/* !!! show the context? */
|
/* !!! show the context? */
|
||||||
copyContext(v, context);
|
copyContext(v, context);
|
||||||
doc.writeEmptyElement("string", singletonAttrs("value", v.c_str()));
|
doc.writeEmptyElement("string", singletonAttrs("value", v.string_view()));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case nPath:
|
case nPath:
|
||||||
|
|
@ -102,14 +102,14 @@ static void printValueAsXML(
|
||||||
if (strict)
|
if (strict)
|
||||||
state.forceValue(*a->value, a->pos);
|
state.forceValue(*a->value, a->pos);
|
||||||
if (a->value->type() == nString)
|
if (a->value->type() == nString)
|
||||||
xmlAttrs["drvPath"] = drvPath = a->value->c_str();
|
xmlAttrs["drvPath"] = drvPath = a->value->string_view();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auto a = v.attrs()->get(state.s.outPath)) {
|
if (auto a = v.attrs()->get(state.s.outPath)) {
|
||||||
if (strict)
|
if (strict)
|
||||||
state.forceValue(*a->value, a->pos);
|
state.forceValue(*a->value, a->pos);
|
||||||
if (a->value->type() == nString)
|
if (a->value->type() == nString)
|
||||||
xmlAttrs["outPath"] = a->value->c_str();
|
xmlAttrs["outPath"] = a->value->string_view();
|
||||||
}
|
}
|
||||||
|
|
||||||
XMLOpenElement _(doc, "derivation", xmlAttrs);
|
XMLOpenElement _(doc, "derivation", xmlAttrs);
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ static void parseFlakeInputAttr(EvalState & state, const Attr & attr, fetchers::
|
||||||
#pragma GCC diagnostic ignored "-Wswitch-enum"
|
#pragma GCC diagnostic ignored "-Wswitch-enum"
|
||||||
switch (attr.value->type()) {
|
switch (attr.value->type()) {
|
||||||
case nString:
|
case nString:
|
||||||
attrs.emplace(state.symbols[attr.name], attr.value->c_str());
|
attrs.emplace(state.symbols[attr.name], std::string(attr.value->string_view()));
|
||||||
break;
|
break;
|
||||||
case nBool:
|
case nBool:
|
||||||
attrs.emplace(state.symbols[attr.name], Explicit<bool>{attr.value->boolean()});
|
attrs.emplace(state.symbols[attr.name], Explicit<bool>{attr.value->boolean()});
|
||||||
|
|
@ -177,7 +177,7 @@ static FlakeInput parseFlakeInput(
|
||||||
parseFlakeInputs(state, attr.value, attr.pos, lockRootAttrPath, flakeDir, false).first;
|
parseFlakeInputs(state, attr.value, attr.pos, lockRootAttrPath, flakeDir, false).first;
|
||||||
} else if (attr.name == sFollows) {
|
} else if (attr.name == sFollows) {
|
||||||
expectType(state, nString, *attr.value, attr.pos);
|
expectType(state, nString, *attr.value, attr.pos);
|
||||||
auto follows(parseInputAttrPath(attr.value->c_str()));
|
auto follows(parseInputAttrPath(attr.value->string_view()));
|
||||||
follows.insert(follows.begin(), lockRootAttrPath.begin(), lockRootAttrPath.end());
|
follows.insert(follows.begin(), lockRootAttrPath.begin(), lockRootAttrPath.end());
|
||||||
input.follows = follows;
|
input.follows = follows;
|
||||||
} else
|
} else
|
||||||
|
|
@ -264,7 +264,7 @@ static Flake readFlake(
|
||||||
|
|
||||||
if (auto description = vInfo.attrs()->get(state.s.description)) {
|
if (auto description = vInfo.attrs()->get(state.s.description)) {
|
||||||
expectType(state, nString, *description->value, description->pos);
|
expectType(state, nString, *description->value, description->pos);
|
||||||
flake.description = description->value->c_str();
|
flake.description = description->value->string_view();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto sInputs = state.symbols.create("inputs");
|
auto sInputs = state.symbols.create("inputs");
|
||||||
|
|
|
||||||
|
|
@ -153,9 +153,9 @@ nix_err nix_err_code(const nix_c_context * read_context)
|
||||||
}
|
}
|
||||||
|
|
||||||
// internal
|
// internal
|
||||||
nix_err call_nix_get_string_callback(const std::string str, nix_get_string_callback callback, void * user_data)
|
nix_err call_nix_get_string_callback(const std::string_view str, nix_get_string_callback callback, void * user_data)
|
||||||
{
|
{
|
||||||
callback(str.c_str(), str.size(), user_data);
|
callback(str.data(), str.size(), user_data);
|
||||||
return NIX_OK;
|
return NIX_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ nix_err nix_context_error(nix_c_context * context);
|
||||||
* @return NIX_OK if there were no errors.
|
* @return NIX_OK if there were no errors.
|
||||||
* @see nix_get_string_callback
|
* @see nix_get_string_callback
|
||||||
*/
|
*/
|
||||||
nix_err call_nix_get_string_callback(const std::string str, nix_get_string_callback callback, void * user_data);
|
nix_err call_nix_get_string_callback(const std::string_view str, nix_get_string_callback callback, void * user_data);
|
||||||
|
|
||||||
#define NIXC_CATCH_ERRS \
|
#define NIXC_CATCH_ERRS \
|
||||||
catch (...) \
|
catch (...) \
|
||||||
|
|
|
||||||
|
|
@ -1228,7 +1228,7 @@ static void opQuery(Globals & globals, Strings opFlags, Strings opArgs)
|
||||||
else {
|
else {
|
||||||
if (v->type() == nString) {
|
if (v->type() == nString) {
|
||||||
attrs2["type"] = "string";
|
attrs2["type"] = "string";
|
||||||
attrs2["value"] = v->c_str();
|
attrs2["value"] = v->string_view();
|
||||||
xml.writeEmptyElement("meta", attrs2);
|
xml.writeEmptyElement("meta", attrs2);
|
||||||
} else if (v->type() == nInt) {
|
} else if (v->type() == nInt) {
|
||||||
attrs2["type"] = "int";
|
attrs2["type"] = "int";
|
||||||
|
|
@ -1249,7 +1249,7 @@ static void opQuery(Globals & globals, Strings opFlags, Strings opArgs)
|
||||||
if (elem->type() != nString)
|
if (elem->type() != nString)
|
||||||
continue;
|
continue;
|
||||||
XMLAttrs attrs3;
|
XMLAttrs attrs3;
|
||||||
attrs3["value"] = elem->c_str();
|
attrs3["value"] = elem->string_view();
|
||||||
xml.writeEmptyElement("string", attrs3);
|
xml.writeEmptyElement("string", attrs3);
|
||||||
}
|
}
|
||||||
} else if (v->type() == nAttrs) {
|
} else if (v->type() == nAttrs) {
|
||||||
|
|
@ -1260,7 +1260,7 @@ static void opQuery(Globals & globals, Strings opFlags, Strings opArgs)
|
||||||
continue;
|
continue;
|
||||||
XMLAttrs attrs3;
|
XMLAttrs attrs3;
|
||||||
attrs3["type"] = globals.state->symbols[i.name];
|
attrs3["type"] = globals.state->symbols[i.name];
|
||||||
attrs3["value"] = i.value->c_str();
|
attrs3["value"] = i.value->string_view();
|
||||||
xml.writeEmptyElement("string", attrs3);
|
xml.writeEmptyElement("string", attrs3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue