mirror of https://github.com/oxen-io/lokinet
Merge branch 'staging' into use-lokid-seed
commit
9991d17976
@ -1 +1,146 @@
|
||||
#include <util/ini.hpp>
|
||||
#include <fstream>
|
||||
#include <list>
|
||||
#include <iostream>
|
||||
|
||||
namespace llarp
|
||||
{
|
||||
bool
|
||||
ConfigParser::LoadFile(const char* fname)
|
||||
{
|
||||
{
|
||||
std::ifstream f(fname);
|
||||
if(!f.is_open())
|
||||
return false;
|
||||
f.seekg(0, std::ios::end);
|
||||
m_Data.resize(f.tellg());
|
||||
f.seekg(0, std::ios::beg);
|
||||
if(m_Data.size() == 0)
|
||||
return false;
|
||||
f.read(m_Data.data(), m_Data.size());
|
||||
}
|
||||
return Parse();
|
||||
}
|
||||
|
||||
bool
|
||||
ConfigParser::LoadString(const std::string& str)
|
||||
{
|
||||
m_Data.resize(str.size());
|
||||
std::copy(str.begin(), str.end(), m_Data.begin());
|
||||
return Parse();
|
||||
}
|
||||
|
||||
void
|
||||
ConfigParser::Clear()
|
||||
{
|
||||
m_Config.clear();
|
||||
m_Data.clear();
|
||||
}
|
||||
|
||||
static bool
|
||||
whitespace(char ch)
|
||||
{
|
||||
return std::isspace(static_cast< unsigned char >(ch)) != 0;
|
||||
}
|
||||
|
||||
bool
|
||||
ConfigParser::Parse()
|
||||
{
|
||||
std::list< String_t > lines;
|
||||
{
|
||||
auto itr = m_Data.begin();
|
||||
// split into lines
|
||||
while(itr != m_Data.end())
|
||||
{
|
||||
auto beg = itr;
|
||||
while(itr != m_Data.end() && *itr != '\n' && *itr != '\r')
|
||||
++itr;
|
||||
lines.emplace_back(std::addressof(*beg), std::distance(beg, itr));
|
||||
if(itr == m_Data.end())
|
||||
break;
|
||||
++itr;
|
||||
}
|
||||
}
|
||||
|
||||
String_t sectName;
|
||||
|
||||
for(const auto& line : lines)
|
||||
{
|
||||
String_t realLine;
|
||||
auto comment = line.find_first_of(';');
|
||||
if(comment == String_t::npos)
|
||||
comment = line.find_first_of('#');
|
||||
if(comment == String_t::npos)
|
||||
realLine = line;
|
||||
else
|
||||
realLine = line.substr(0, comment);
|
||||
// blank or commented line?
|
||||
if(realLine.size() == 0)
|
||||
continue;
|
||||
// find delimiters
|
||||
auto sectOpenPos = realLine.find_first_of('[');
|
||||
auto sectClosPos = realLine.find_first_of(']');
|
||||
auto kvDelim = realLine.find_first_of('=');
|
||||
if(sectOpenPos != String_t::npos && sectClosPos != String_t::npos
|
||||
&& kvDelim == String_t::npos)
|
||||
{
|
||||
// section header
|
||||
|
||||
// clamp whitespaces
|
||||
++sectOpenPos;
|
||||
while(whitespace(realLine[sectOpenPos]) && sectOpenPos != sectClosPos)
|
||||
++sectOpenPos;
|
||||
--sectClosPos;
|
||||
while(whitespace(realLine[sectClosPos]) && sectClosPos != sectOpenPos)
|
||||
--sectClosPos;
|
||||
// set section name
|
||||
sectName = realLine.substr(sectOpenPos, sectClosPos);
|
||||
}
|
||||
else if(kvDelim != String_t::npos)
|
||||
{
|
||||
// key value pair
|
||||
String_t::size_type k_start = 0;
|
||||
String_t::size_type k_end = kvDelim;
|
||||
String_t::size_type v_start = kvDelim + 1;
|
||||
String_t::size_type v_end = realLine.size() - 1;
|
||||
// clamp whitespaces
|
||||
while(whitespace(realLine[k_start]) && k_start != kvDelim)
|
||||
++k_start;
|
||||
while(whitespace(realLine[k_end]) && k_end != k_start)
|
||||
--k_end;
|
||||
while(whitespace(realLine[v_start]) && v_start != v_end)
|
||||
++v_start;
|
||||
while(whitespace(realLine[v_end]))
|
||||
--v_end;
|
||||
|
||||
// sect.k = v
|
||||
String_t k = realLine.substr(k_start, k_end - k_start);
|
||||
String_t v = realLine.substr(v_start, 1 + (v_end - v_start));
|
||||
Section_t& sect = m_Config[sectName];
|
||||
sect.emplace(k, v);
|
||||
}
|
||||
else // malformed?
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
ConfigParser::IterAll(
|
||||
std::function< void(const String_t&, const Section_t&) > visit)
|
||||
{
|
||||
for(const auto& item : m_Config)
|
||||
visit(item.first, item.second);
|
||||
}
|
||||
|
||||
bool
|
||||
ConfigParser::VisitSection(
|
||||
const char* name,
|
||||
std::function< bool(const Section_t& sect) > visit) const
|
||||
{
|
||||
auto itr = m_Config.find(name);
|
||||
if(itr == m_Config.end())
|
||||
return false;
|
||||
return visit(itr->second);
|
||||
}
|
||||
} // namespace llarp
|
||||
|
@ -1,446 +1,52 @@
|
||||
/**
|
||||
* The MIT License (MIT)
|
||||
* Copyright (c) <2015> <carriez.md@gmail.com>
|
||||
* Copyright (c) <2018> <rtharp@customwebapps.com>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to
|
||||
* deal in the Software without restriction, including without limitation the
|
||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef INI_HPP
|
||||
#define INI_HPP
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#ifndef LOKINET_BOOTSERV_CONFIG_HPP
|
||||
#define LOKINET_BOOTSERV_CONFIG_HPP
|
||||
#include <unordered_map>
|
||||
#include <util/string_view.hpp>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace ini
|
||||
namespace llarp
|
||||
{
|
||||
struct Level
|
||||
struct ConfigParser
|
||||
{
|
||||
Level() : parent(nullptr), depth(0)
|
||||
{
|
||||
}
|
||||
Level(Level *p) : parent(p), depth(0)
|
||||
{
|
||||
}
|
||||
|
||||
using value_map_t = std::list< std::pair< std::string, std::string > >;
|
||||
using section_map_t = std::map< std::string, Level >;
|
||||
using sections_t = std::list< section_map_t::const_iterator >;
|
||||
value_map_t values;
|
||||
section_map_t sections;
|
||||
sections_t ordered_sections;
|
||||
Level *parent;
|
||||
size_t depth;
|
||||
using String_t = llarp::string_view;
|
||||
using Section_t = std::unordered_multimap< String_t, String_t >;
|
||||
using Config_impl_t = std::unordered_map< String_t, Section_t >;
|
||||
/// clear parser
|
||||
void
|
||||
Clear();
|
||||
|
||||
static std::string default_value;
|
||||
/// load config file for bootserv
|
||||
/// return true on success
|
||||
/// return false on error
|
||||
bool
|
||||
LoadFile(const char* fname);
|
||||
|
||||
const std::string &operator[](const std::string &name)
|
||||
{
|
||||
for(const auto &itr : values)
|
||||
if(itr.first == name)
|
||||
return itr.second;
|
||||
return default_value;
|
||||
}
|
||||
Level &
|
||||
operator()(const std::string &name)
|
||||
{
|
||||
return sections[name];
|
||||
}
|
||||
};
|
||||
/// load from string
|
||||
/// return true on success
|
||||
/// return false on error
|
||||
bool
|
||||
LoadString(const std::string& str);
|
||||
|
||||
class Parser
|
||||
{
|
||||
public:
|
||||
Parser(const std::string &fname) : Parser(fname.c_str())
|
||||
{
|
||||
}
|
||||
Parser(const char *fn);
|
||||
Parser(std::istream &f) : f_(&f), ln_(0)
|
||||
{
|
||||
parse(top_);
|
||||
}
|
||||
Level &
|
||||
top()
|
||||
{
|
||||
return top_;
|
||||
}
|
||||
void
|
||||
dump(std::ostream &s)
|
||||
{
|
||||
dump(s, top(), "");
|
||||
}
|
||||
/// iterate all sections and thier values
|
||||
void
|
||||
print()
|
||||
{
|
||||
dump(std::cout, top(), "");
|
||||
}
|
||||
IterAll(std::function< void(const String_t&, const Section_t&) > visit);
|
||||
|
||||
/// visit a section in config read only by name
|
||||
/// return false if no section or value propagated from visitor
|
||||
bool
|
||||
write(const std::string filename)
|
||||
{
|
||||
// this->print();
|
||||
// printf("parser::Write\n");
|
||||
std::ofstream s;
|
||||
s.open(filename);
|
||||
if(!s.is_open())
|
||||
{
|
||||
printf("parser::Write - can't open\n");
|
||||
err("Cant open");
|
||||
return false;
|
||||
}
|
||||
// reset read
|
||||
// printf("parser::Write - committing\n");
|
||||
this->commitStream(s);
|
||||
s.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
commitStream(std::ostream &s)
|
||||
{
|
||||
// printf("parser::commitStream - seekd\n");
|
||||
this->ln_ = 0;
|
||||
this->f_->clear();
|
||||
this->f_->seekg(0);
|
||||
// printf("parser::commitStream - reading top\n");
|
||||
std::vector< std::string > savedSections;
|
||||
this->commit(s, top_, savedSections, false);
|
||||
}
|
||||
VisitSection(const char* name,
|
||||
std::function< bool(const Section_t&) > visit) const;
|
||||
|
||||
private:
|
||||
void
|
||||
dump(std::ostream &s, const Level &l, const std::string &sname);
|
||||
void
|
||||
parse(Level &l);
|
||||
void
|
||||
commit(std::ostream &s, Level &l, std::vector< std::string > &savedSections,
|
||||
bool disabledSection);
|
||||
void
|
||||
parseSLine(std::string &sname, size_t &depth);
|
||||
void
|
||||
err(const char *s);
|
||||
bool
|
||||
Parse();
|
||||
|
||||
private:
|
||||
Level top_;
|
||||
std::ifstream f0_;
|
||||
std::istream *f_;
|
||||
std::string line_;
|
||||
size_t ln_;
|
||||
std::vector< char > m_Data;
|
||||
Config_impl_t m_Config;
|
||||
};
|
||||
|
||||
inline void
|
||||
Parser::err(const char *s)
|
||||
{
|
||||
char buf[256];
|
||||
sprintf(buf, "%s on line #%zu", s, ln_);
|
||||
}
|
||||
|
||||
inline std::string
|
||||
trim(const std::string &s)
|
||||
{
|
||||
char p[] = " \t\r\n";
|
||||
long sp = 0;
|
||||
long ep = s.length() - 1;
|
||||
for(; sp <= ep; ++sp)
|
||||
if(!strchr(p, s[sp]))
|
||||
break;
|
||||
for(; ep >= 0; --ep)
|
||||
if(!strchr(p, s[ep]))
|
||||
break;
|
||||
return s.substr(sp, ep - sp + 1);
|
||||
}
|
||||
|
||||
inline Parser::Parser(const char *fn) : f0_(fn), f_(&f0_), ln_(0)
|
||||
{
|
||||
if(f0_)
|
||||
{
|
||||
parse(top_);
|
||||
}
|
||||
}
|
||||
|
||||
inline void
|
||||
Parser::parseSLine(std::string &sname, size_t &depth)
|
||||
{
|
||||
depth = 0;
|
||||
for(; depth < line_.length(); ++depth)
|
||||
if(line_[depth] != '[')
|
||||
break;
|
||||
|
||||
sname = line_.substr(depth, line_.length() - 2 * depth);
|
||||
}
|
||||
|
||||
inline void
|
||||
Parser::parse(Level &l)
|
||||
{
|
||||
while(std::getline(*f_, line_))
|
||||
{
|
||||
++ln_;
|
||||
if(line_[0] == '#' || line_[0] == ';')
|
||||
continue;
|
||||
line_ = trim(line_);
|
||||
if(line_.empty())
|
||||
continue;
|
||||
if(line_[0] == '[')
|
||||
{
|
||||
size_t depth;
|
||||
std::string sname;
|
||||
parseSLine(sname, depth);
|
||||
Level *lp = nullptr;
|
||||
Level *parent = &l;
|
||||
if(depth > l.depth + 1)
|
||||
err("section with wrong depth");
|
||||
if(l.depth == depth - 1)
|
||||
lp = &l.sections[sname];
|
||||
else
|
||||
{
|
||||
lp = l.parent;
|
||||
size_t n = l.depth - depth;
|
||||
for(size_t i = 0; i < n; ++i)
|
||||
lp = lp->parent;
|
||||
parent = lp;
|
||||
lp = &lp->sections[sname];
|
||||
}
|
||||
if(lp->depth != 0)
|
||||
err("duplicate section name on the same level");
|
||||
if(!lp->parent)
|
||||
{
|
||||
lp->depth = depth;
|
||||
lp->parent = parent;
|
||||
}
|
||||
parent->ordered_sections.push_back(parent->sections.find(sname));
|
||||
parse(*lp);
|
||||
}
|
||||
else
|
||||
{
|
||||
size_t n = line_.find('=');
|
||||
if(n == std::string::npos)
|
||||
err("no '=' found");
|
||||
|
||||
auto p =
|
||||
std::make_pair(trim(line_.substr(0, n)),
|
||||
trim(line_.substr(n + 1, line_.length() - n - 1)));
|
||||
l.values.push_back(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline void
|
||||
saveValues(std::ostream &s, std::vector< std::string > excludes, Level &l)
|
||||
{
|
||||
// printf("checking keys[%lu] against [%lu]\n", l.values.size(),
|
||||
// excludes.size());
|
||||
for(auto it = l.values.begin(); it != l.values.end(); ++it)
|
||||
{
|
||||
// printf("key[%s]\n", it->first.c_str());
|
||||
auto check = find(excludes.begin(), excludes.end(), it->first);
|
||||
if(check == excludes.end())
|
||||
{
|
||||
// printf("We didnt write it [%s=%s]\n", it->first.c_str(),
|
||||
// it->second.c_str());
|
||||
s << it->first + "=" + it->second << "\n"; // commit to stream
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline void
|
||||
Parser::commit(std::ostream &s, Level &l,
|
||||
std::vector< std::string > &savedSections,
|
||||
bool disabledSection)
|
||||
{
|
||||
std::vector< std::string > keys;
|
||||
bool keysChecked = false;
|
||||
while(std::getline(*this->f_, line_))
|
||||
{
|
||||
++ln_;
|
||||
if(line_[0] == '#' || line_[0] == ';')
|
||||
{
|
||||
s << line_ << "\n"; // commit to stream
|
||||
continue;
|
||||
}
|
||||
std::string tline_ = trim(line_);
|
||||
if(tline_.empty())
|
||||
{
|
||||
s << line_ << "\n"; // commit to stream
|
||||
continue;
|
||||
}
|
||||
if(tline_[0] == '[')
|
||||
{
|
||||
bool disableNextSection = false;
|
||||
size_t depth;
|
||||
std::string sname;
|
||||
parseSLine(sname, depth);
|
||||
s << "[" << sname << "]"
|
||||
<< "\n"; // commit to stream
|
||||
|
||||
auto test = this->top_.sections.find(sname);
|
||||
if(test == this->top_.sections.end())
|
||||
{
|
||||
// could mean we're done with this section
|
||||
// printf("We dont have section [%s]\n", sname.c_str());
|
||||
// we'll comment out these keys since we've intentionally dropped them
|
||||
disableNextSection = true;
|
||||
}
|
||||
|
||||
Level *lp = NULL;
|
||||
Level *parent = &l;
|
||||
if(depth > l.depth + 1)
|
||||
err("section with wrong depth");
|
||||
|
||||
// if depth is one level deep
|
||||
if(l.depth == depth - 1)
|
||||
{
|
||||
// make level point to one of our sections
|
||||
lp = &l.sections[sname];
|
||||
}
|
||||
else
|
||||
{
|
||||
// find the parent by depth
|
||||
lp = l.parent;
|
||||
size_t n = l.depth - depth;
|
||||
for(size_t i = 0; i < n; ++i)
|
||||
lp = lp->parent;
|
||||
parent = lp;
|
||||
lp = &lp->sections[sname];
|
||||
}
|
||||
/*
|
||||
if(lp->depth != 0)
|
||||
{
|
||||
printf("has depth still, found [%s] at [%zu]\n", sname.c_str(),
|
||||
depth);
|
||||
}
|
||||
*/
|
||||
if(!lp->parent)
|
||||
{
|
||||
printf("no parent\n");
|
||||
lp->depth = depth;
|
||||
lp->parent = parent;
|
||||
}
|
||||
|
||||
// flush remainder of this section
|
||||
saveValues(s, keys, l);
|
||||
keysChecked = true;
|
||||
|
||||
// start next section
|
||||
this->commit(s, *lp, savedSections, disableNextSection);
|
||||
savedSections.push_back(sname);
|
||||
}
|
||||
else
|
||||
{
|
||||
size_t n = line_.find('=');
|
||||
if(n == std::string::npos)
|
||||
err("no '=' found");
|
||||
|
||||
auto key = trim(line_.substr(0, n));
|
||||
keys.push_back(key);
|
||||
auto val = std::find_if(
|
||||
l.values.begin(), l.values.end(),
|
||||
[&key](const std::pair< std::string, std::string > &element) {
|
||||
return element.first == key;
|
||||
});
|
||||
if(val != l.values.end())
|
||||
{
|
||||
if(val->second.c_str()
|
||||
== trim(line_.substr(n + 1, line_.length() - n - 1)))
|
||||
{
|
||||
// copying line
|
||||
if(disabledSection)
|
||||
s << "# ";
|
||||
s << line_ << "\n"; // commit to stream
|
||||
}
|
||||
else
|
||||
{
|
||||
// update value
|
||||
if(disabledSection)
|
||||
s << "# ";
|
||||
s << line_.substr(0, n) + "=" + val->second
|
||||
<< "\n"; // commit to stream
|
||||
}
|
||||
} /*
|
||||
else
|
||||
{
|
||||
// remove it
|
||||
//printf("kv found [%s] no current\n", key.c_str());
|
||||
} */
|
||||
}
|
||||
}
|
||||
|
||||
// handle last section
|
||||
if(!keysChecked)
|
||||
{
|
||||
saveValues(s, keys, l);
|
||||
}
|
||||
|
||||
// we're at the main level and have the list of sections
|
||||
if(l.sections.size())
|
||||
{
|
||||
// check to make sure we've written out all the sections we need to
|
||||
// printf("sections old[%lu] run[%lu]\n", savedSections.size(),
|
||||
// l.sections.size());
|
||||
for(auto it = l.sections.begin(); it != l.sections.end(); ++it)
|
||||
{
|
||||
// printf("sections[%s]\n", it->first.c_str());
|
||||
auto check =
|
||||
find(savedSections.begin(), savedSections.end(), it->first);
|
||||
if(check == savedSections.end())
|
||||
{
|
||||
// printf("Adding section [%s]\n", it->first.c_str());
|
||||
// s << "[" << it->first + "]" << "\n"; // commit to stream
|
||||
dump(s, l.sections[it->first], it->first);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline void
|
||||
Parser::dump(std::ostream &s, const Level &l, const std::string &sname)
|
||||
{
|
||||
if(!sname.empty())
|
||||
s << '\n';
|
||||
for(size_t i = 0; i < l.depth; ++i)
|
||||
s << '[';
|
||||
if(!sname.empty())
|
||||
s << sname;
|
||||
for(size_t i = 0; i < l.depth; ++i)
|
||||
s << ']';
|
||||
if(!sname.empty())
|
||||
s << std::endl;
|
||||
|
||||
for(const auto &itr : l.values)
|
||||
s << itr.first << '=' << itr.second << std::endl;
|
||||
|
||||
for(Level::sections_t::const_iterator it = l.ordered_sections.begin();
|
||||
it != l.ordered_sections.end(); ++it)
|
||||
{
|
||||
assert((*it)->second.depth == l.depth + 1);
|
||||
dump(s, (*it)->second, (*it)->first);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ini
|
||||
} // namespace llarp
|
||||
|
||||
#endif // INI_HPP
|
||||
#endif
|
||||
|
@ -0,0 +1,52 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <util/ini.hpp>
|
||||
|
||||
struct TestINIParser : public ::testing::Test
|
||||
{
|
||||
llarp::ConfigParser parser;
|
||||
|
||||
void TearDown()
|
||||
{
|
||||
parser.Clear();
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(TestINIParser, TestParseEmpty)
|
||||
{
|
||||
ASSERT_TRUE(parser.LoadString(""));
|
||||
}
|
||||
|
||||
TEST_F(TestINIParser, TestParseOneSection)
|
||||
{
|
||||
llarp::ConfigParser::Section_t sect;
|
||||
// this is an anti pattern don't write this kind of code with configpaser
|
||||
auto assertVisit = [§](const auto & section) -> bool {
|
||||
sect = section;
|
||||
return true;
|
||||
};
|
||||
ASSERT_TRUE(parser.LoadString("[test]\nkey=val \n"));
|
||||
ASSERT_TRUE(parser.VisitSection("test", assertVisit));
|
||||
auto itr = sect.find("notfound");
|
||||
ASSERT_EQ(itr, sect.end());
|
||||
itr = sect.find("key");
|
||||
ASSERT_NE(itr, sect.end());
|
||||
ASSERT_STREQ(itr->second.c_str(), "val");
|
||||
}
|
||||
|
||||
TEST_F(TestINIParser, TestParseSectionDuplicateKeys)
|
||||
{
|
||||
ASSERT_TRUE(parser.LoadString("[test]\nkey1=val1\nkey1=val2"));
|
||||
size_t num = 0;
|
||||
auto visit =[&num](const auto & section) -> bool {
|
||||
num = section.count("key1");
|
||||
return true;
|
||||
};
|
||||
ASSERT_TRUE(parser.VisitSection("test", visit));
|
||||
ASSERT_EQ(num, size_t(2));
|
||||
}
|
||||
|
||||
TEST_F(TestINIParser, TestParseInvalid)
|
||||
{
|
||||
ASSERT_FALSE(parser.LoadString("srged5ghe5\nf34wtge5\nw34tgfs4ygsd5yg=4;\n#g4syhgd5\n"));
|
||||
}
|
Loading…
Reference in New Issue