2015-04-13 04:09:02 -05:00
|
|
|
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
|
2015-03-17 18:56:15 -05:00
|
|
|
/*
|
|
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
*/
|
|
|
|
|
2017-12-20 07:06:26 -06:00
|
|
|
#include <config.h>
|
2016-04-15 09:07:24 -05:00
|
|
|
|
2017-03-08 10:38:22 -06:00
|
|
|
#include "Util.hpp"
|
|
|
|
|
2016-08-30 02:06:39 -05:00
|
|
|
#include <csignal>
|
2015-12-13 11:04:45 -06:00
|
|
|
#include <sys/poll.h>
|
2018-08-29 10:47:32 -05:00
|
|
|
#ifdef __linux
|
2020-05-02 13:13:14 -05:00
|
|
|
# include <sys/prctl.h>
|
|
|
|
# include <sys/syscall.h>
|
|
|
|
# include <sys/vfs.h>
|
|
|
|
# include <sys/resource.h>
|
2020-11-08 09:36:41 -06:00
|
|
|
#elif defined __FreeBSD__
|
|
|
|
# include <sys/resource.h>
|
2018-09-05 07:11:05 -05:00
|
|
|
#elif defined IOS
|
2018-08-29 10:47:32 -05:00
|
|
|
#import <Foundation/Foundation.h>
|
|
|
|
#endif
|
2016-09-29 09:47:28 -05:00
|
|
|
#include <sys/stat.h>
|
2016-04-08 07:22:22 -05:00
|
|
|
#include <sys/uio.h>
|
2018-01-29 09:13:54 -06:00
|
|
|
#include <sys/types.h>
|
2016-04-20 08:43:00 -05:00
|
|
|
#include <unistd.h>
|
2018-01-29 09:13:54 -06:00
|
|
|
#include <dirent.h>
|
2020-04-18 05:55:50 -05:00
|
|
|
#include <fcntl.h>
|
2015-12-13 11:04:45 -06:00
|
|
|
|
2016-04-20 08:43:00 -05:00
|
|
|
#include <atomic>
|
2016-03-08 01:31:29 -06:00
|
|
|
#include <cassert>
|
2016-09-27 07:48:32 -05:00
|
|
|
#include <chrono>
|
|
|
|
#include <cstdio>
|
2015-03-28 06:53:44 -05:00
|
|
|
#include <cstdlib>
|
2015-04-09 17:25:48 -05:00
|
|
|
#include <cstring>
|
2019-10-25 19:48:05 -05:00
|
|
|
#include <ctime>
|
2016-09-27 07:48:32 -05:00
|
|
|
#include <fstream>
|
2015-11-06 04:46:31 -06:00
|
|
|
#include <iomanip>
|
loolwsd: include cleanup and organization
A source file (.cpp) must include its own header first.
This insures that the header is self-contained and
doesn't depend on arbitrary (and accidental) includes
before it to compile.
Furthermore, system headers should go next, followed by
C then C++ headers, then libraries (Poco, etc) and, finally,
project headers come last.
This makes sure that headers and included in the same dependency
order to avoid side-effects. For example, Poco should never rely on
anything from our project in the same way that a C header should
never rely on anything in C++, Poco, or project headers.
Also, includes ought to be sorted where possible, to improve
readability and avoid accidental duplicates (of which there
were a few).
Change-Id: I62cc1343e4a091d69195e37ed659dba20cfcb1ef
Reviewed-on: https://gerrit.libreoffice.org/25262
Reviewed-by: Ashod Nakashian <ashnakash@gmail.com>
Tested-by: Ashod Nakashian <ashnakash@gmail.com>
2016-05-21 09:23:07 -05:00
|
|
|
#include <iostream>
|
2016-03-08 01:31:29 -06:00
|
|
|
#include <mutex>
|
2019-04-14 13:14:12 -05:00
|
|
|
#include <unordered_map>
|
2016-03-08 01:31:29 -06:00
|
|
|
#include <random>
|
2015-11-06 04:46:31 -06:00
|
|
|
#include <sstream>
|
2015-03-17 18:56:15 -05:00
|
|
|
#include <string>
|
2016-11-13 15:12:01 -06:00
|
|
|
#include <thread>
|
2015-03-17 18:56:15 -05:00
|
|
|
|
2016-05-30 19:42:44 -05:00
|
|
|
#include <Poco/Base64Encoder.h>
|
2019-05-16 15:12:20 -05:00
|
|
|
#include <Poco/HexBinaryEncoder.h>
|
2016-03-08 01:31:29 -06:00
|
|
|
#include <Poco/ConsoleChannel.h>
|
2015-04-22 03:14:11 -05:00
|
|
|
#include <Poco/Exception.h>
|
2015-04-27 06:16:37 -05:00
|
|
|
#include <Poco/Format.h>
|
2017-05-28 11:20:49 -05:00
|
|
|
#include <Poco/JSON/JSON.h>
|
|
|
|
#include <Poco/JSON/Object.h>
|
|
|
|
#include <Poco/JSON/Parser.h>
|
2016-05-30 19:42:44 -05:00
|
|
|
#include <Poco/RandomStream.h>
|
loolwsd: include cleanup and organization
A source file (.cpp) must include its own header first.
This insures that the header is self-contained and
doesn't depend on arbitrary (and accidental) includes
before it to compile.
Furthermore, system headers should go next, followed by
C then C++ headers, then libraries (Poco, etc) and, finally,
project headers come last.
This makes sure that headers and included in the same dependency
order to avoid side-effects. For example, Poco should never rely on
anything from our project in the same way that a C header should
never rely on anything in C++, Poco, or project headers.
Also, includes ought to be sorted where possible, to improve
readability and avoid accidental duplicates (of which there
were a few).
Change-Id: I62cc1343e4a091d69195e37ed659dba20cfcb1ef
Reviewed-on: https://gerrit.libreoffice.org/25262
Reviewed-by: Ashod Nakashian <ashnakash@gmail.com>
Tested-by: Ashod Nakashian <ashnakash@gmail.com>
2016-05-21 09:23:07 -05:00
|
|
|
#include <Poco/TemporaryFile.h>
|
2015-04-22 03:14:11 -05:00
|
|
|
#include <Poco/Util/Application.h>
|
2015-03-17 18:56:15 -05:00
|
|
|
|
2016-03-08 01:31:29 -06:00
|
|
|
#include "Common.hpp"
|
2020-09-01 12:15:30 -05:00
|
|
|
#include "Log.hpp"
|
2020-07-02 07:36:50 -05:00
|
|
|
#include "Protocol.hpp"
|
2015-03-28 06:53:44 -05:00
|
|
|
|
2015-12-19 08:16:44 -06:00
|
|
|
namespace Util
|
|
|
|
{
|
2017-01-13 06:55:01 -06:00
|
|
|
namespace rng
|
2016-05-30 19:42:44 -05:00
|
|
|
{
|
2017-01-13 06:55:01 -06:00
|
|
|
static std::random_device _rd;
|
|
|
|
static std::mutex _rngMutex;
|
|
|
|
static Poco::RandomBuf _randBuf;
|
|
|
|
|
|
|
|
// Create the prng with a random-device for seed.
|
|
|
|
// If we don't have a hardware random-device, we will get the same seed.
|
|
|
|
// In that case we are better off with an arbitrary, but changing, seed.
|
|
|
|
static std::mt19937_64 _rng = std::mt19937_64(_rd.entropy()
|
|
|
|
? _rd()
|
|
|
|
: (clock() + getpid()));
|
|
|
|
|
|
|
|
// A new seed is used to shuffle the sequence.
|
|
|
|
// N.B. Always reseed after getting forked!
|
|
|
|
void reseed()
|
|
|
|
{
|
|
|
|
_rng.seed(_rd.entropy() ? _rd() : (clock() + getpid()));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns a new random number.
|
|
|
|
unsigned getNext()
|
|
|
|
{
|
|
|
|
std::unique_lock<std::mutex> lock(_rngMutex);
|
|
|
|
return _rng();
|
|
|
|
}
|
|
|
|
|
2020-11-15 11:03:45 -06:00
|
|
|
std::vector<char> getBytes(const std::size_t length)
|
2017-01-13 06:55:01 -06:00
|
|
|
{
|
|
|
|
std::vector<char> v(length);
|
|
|
|
_randBuf.readFromDevice(v.data(), v.size());
|
|
|
|
return v;
|
|
|
|
}
|
|
|
|
|
2019-05-16 15:12:20 -05:00
|
|
|
/// Generate a string of random characters.
|
2020-11-15 11:03:45 -06:00
|
|
|
std::string getHexString(const std::size_t length)
|
2019-05-16 15:12:20 -05:00
|
|
|
{
|
|
|
|
std::stringstream ss;
|
|
|
|
Poco::HexBinaryEncoder hex(ss);
|
|
|
|
hex.write(getBytes(length).data(), length);
|
|
|
|
return ss.str().substr(0, length);
|
|
|
|
}
|
|
|
|
|
2019-06-21 06:34:53 -05:00
|
|
|
/// Generate a string of harder random characters.
|
2020-11-15 11:03:45 -06:00
|
|
|
std::string getHardRandomHexString(const std::size_t length)
|
2019-06-21 06:34:53 -05:00
|
|
|
{
|
|
|
|
std::stringstream ss;
|
|
|
|
Poco::HexBinaryEncoder hex(ss);
|
|
|
|
|
|
|
|
// a poor fallback but something.
|
|
|
|
std::vector<char> random = getBytes(length);
|
|
|
|
int fd = open("/dev/urandom", O_RDONLY);
|
2019-06-21 16:33:56 -05:00
|
|
|
int len = 0;
|
2019-06-21 06:34:53 -05:00
|
|
|
if (fd < 0 ||
|
|
|
|
(len = read(fd, random.data(), length)) < 0 ||
|
2020-11-15 11:03:45 -06:00
|
|
|
std::size_t(len) < length)
|
2019-06-21 06:34:53 -05:00
|
|
|
{
|
|
|
|
LOG_ERR("failed to read " << length << " hard random bytes, got " << len << " for hash: " << errno);
|
|
|
|
}
|
|
|
|
close(fd);
|
|
|
|
hex.write(random.data(), length);
|
|
|
|
return ss.str().substr(0, length);
|
|
|
|
}
|
|
|
|
|
2017-01-13 06:55:01 -06:00
|
|
|
/// Generates a random string in Base64.
|
|
|
|
/// Note: May contain '/' characters.
|
2020-11-15 11:03:45 -06:00
|
|
|
std::string getB64String(const std::size_t length)
|
2017-01-13 06:55:01 -06:00
|
|
|
{
|
|
|
|
std::stringstream ss;
|
|
|
|
Poco::Base64Encoder b64(ss);
|
|
|
|
b64.write(getBytes(length).data(), length);
|
|
|
|
return ss.str().substr(0, length);
|
|
|
|
}
|
|
|
|
|
2020-11-15 11:03:45 -06:00
|
|
|
std::string getFilename(const std::size_t length)
|
2017-01-13 06:55:01 -06:00
|
|
|
{
|
2017-05-07 10:05:34 -05:00
|
|
|
std::string s = getB64String(length * 2);
|
|
|
|
s.erase(std::remove_if(s.begin(), s.end(),
|
|
|
|
[](const std::string::value_type& c)
|
|
|
|
{
|
|
|
|
// Remove undesirable characters in a filename.
|
|
|
|
return c == '/' || c == ' ' || c == '+';
|
|
|
|
}),
|
|
|
|
s.end());
|
2017-01-13 06:55:01 -06:00
|
|
|
return s.substr(0, length);
|
|
|
|
}
|
2016-05-30 19:42:44 -05:00
|
|
|
}
|
|
|
|
|
2018-03-14 10:46:52 -05:00
|
|
|
static std::string getDefaultTmpDir()
|
|
|
|
{
|
|
|
|
const char *tmp = getenv("TMPDIR");
|
|
|
|
if (!tmp)
|
|
|
|
tmp = getenv("TEMP");
|
|
|
|
if (!tmp)
|
|
|
|
tmp = getenv("TMP");
|
|
|
|
if (!tmp)
|
|
|
|
tmp = "/tmp";
|
|
|
|
return tmp;
|
|
|
|
}
|
|
|
|
|
wsd: faster jail setup via bind-mount
loolmount now works and supports mounting and
unmounting, plus numerous improvements,
refactoring, logging, etc.. When enabled,
binding improves the jail setup time by anywhere
from 2x to orders of magnitude (in docker, f.e.).
A new config entry mount_jail_tree controls
whether mounting is used or the old method of
linking/copying of jail contents. It is set to
true by default and falls back to linking/copying.
A test mount is done when the setting is enabled,
and if mounting fails, it's disabled to avoid noise.
Temporarily disabled for unit-tests until we can
cleanup lingering mounts after Jenkins aborts our
build job. In a future patch we will have mount/jail
cleanup as part of make.
The network/system files in /etc that need frequent
refreshing are now updated in systemplate to make
their most recent version available in the jails.
These files can change during the course of loolwsd
lifetime, and are unlikely to be updated in
systemplate after installation at all. We link to
them in the systemplate/etc directory, and if that
fails, we copy them before forking each kit
instance to have the latest.
This reworks the approach used to bind-mount the
jails and the templates such that the total is
now down to only three mounts: systemplate, lo, tmp.
As now systemplate and lotemplate are shared, they
must be mounted as readonly, this means that user/
must now be moved into tmp/user/ which is writable.
The mount-points must be recursive, because we mount
lo/ within the mount-point of systemplate (which is
the root of the jail). But because we (re)bind
recursively, and because both systemplate and
lotemplate are mounted for each jails, we need to
make them unbindable, so they wouldn't multiply the
mount-points for each jails (an explosive growth!)
Contrarywise, we don't want the mount-points to
be shared, because we don't expect to add/remove
mounts after a jail is created.
The random temp directory is now created and set
correctly, plus many logging and other improvements.
Change-Id: Iae3fda5e876cf47d2cae6669a87b5b826a8748df
Reviewed-on: https://gerrit.libreoffice.org/c/online/+/92829
Tested-by: Jenkins
Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice@gmail.com>
Reviewed-by: Ashod Nakashian <ashnakash@gmail.com>
2020-04-09 08:02:58 -05:00
|
|
|
std::string createRandomTmpDir(std::string root)
|
2018-03-14 10:46:52 -05:00
|
|
|
{
|
wsd: faster jail setup via bind-mount
loolmount now works and supports mounting and
unmounting, plus numerous improvements,
refactoring, logging, etc.. When enabled,
binding improves the jail setup time by anywhere
from 2x to orders of magnitude (in docker, f.e.).
A new config entry mount_jail_tree controls
whether mounting is used or the old method of
linking/copying of jail contents. It is set to
true by default and falls back to linking/copying.
A test mount is done when the setting is enabled,
and if mounting fails, it's disabled to avoid noise.
Temporarily disabled for unit-tests until we can
cleanup lingering mounts after Jenkins aborts our
build job. In a future patch we will have mount/jail
cleanup as part of make.
The network/system files in /etc that need frequent
refreshing are now updated in systemplate to make
their most recent version available in the jails.
These files can change during the course of loolwsd
lifetime, and are unlikely to be updated in
systemplate after installation at all. We link to
them in the systemplate/etc directory, and if that
fails, we copy them before forking each kit
instance to have the latest.
This reworks the approach used to bind-mount the
jails and the templates such that the total is
now down to only three mounts: systemplate, lo, tmp.
As now systemplate and lotemplate are shared, they
must be mounted as readonly, this means that user/
must now be moved into tmp/user/ which is writable.
The mount-points must be recursive, because we mount
lo/ within the mount-point of systemplate (which is
the root of the jail). But because we (re)bind
recursively, and because both systemplate and
lotemplate are mounted for each jails, we need to
make them unbindable, so they wouldn't multiply the
mount-points for each jails (an explosive growth!)
Contrarywise, we don't want the mount-points to
be shared, because we don't expect to add/remove
mounts after a jail is created.
The random temp directory is now created and set
correctly, plus many logging and other improvements.
Change-Id: Iae3fda5e876cf47d2cae6669a87b5b826a8748df
Reviewed-on: https://gerrit.libreoffice.org/c/online/+/92829
Tested-by: Jenkins
Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice@gmail.com>
Reviewed-by: Ashod Nakashian <ashnakash@gmail.com>
2020-04-09 08:02:58 -05:00
|
|
|
if (root.empty())
|
|
|
|
root = getDefaultTmpDir();
|
|
|
|
const std::string newTmp = root + "/lool-" + rng::getFilename(16);
|
|
|
|
if (::mkdir(newTmp.c_str(), S_IRWXU) < 0)
|
|
|
|
{
|
|
|
|
LOG_SYS("Failed to create random temp directory [" << newTmp << "]");
|
|
|
|
return root;
|
2018-03-14 10:46:52 -05:00
|
|
|
}
|
|
|
|
return newTmp;
|
|
|
|
}
|
|
|
|
|
2019-02-12 05:16:40 -06:00
|
|
|
#if !MOBILEAPP
|
2018-04-16 14:03:01 -05:00
|
|
|
int getProcessThreadCount()
|
|
|
|
{
|
|
|
|
DIR *fdDir = opendir("/proc/self/task");
|
|
|
|
if (!fdDir)
|
|
|
|
{
|
|
|
|
LOG_ERR("No proc mounted");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
int tasks = 0;
|
2020-01-20 13:33:00 -06:00
|
|
|
struct dirent *i;
|
|
|
|
while ((i = readdir(fdDir)))
|
|
|
|
{
|
|
|
|
if (i->d_name[0] != '.')
|
|
|
|
tasks++;
|
|
|
|
}
|
2018-04-16 14:03:01 -05:00
|
|
|
closedir(fdDir);
|
|
|
|
return tasks;
|
|
|
|
}
|
|
|
|
|
2018-01-29 09:13:54 -06:00
|
|
|
// close what we have - far faster than going up to a 1m open_max eg.
|
2019-11-12 03:50:33 -06:00
|
|
|
static bool closeFdsFromProc(std::map<int, int> *mapFdsToKeep = nullptr)
|
2018-01-29 09:13:54 -06:00
|
|
|
{
|
|
|
|
DIR *fdDir = opendir("/proc/self/fd");
|
|
|
|
if (!fdDir)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
struct dirent *i;
|
|
|
|
|
|
|
|
while ((i = readdir(fdDir))) {
|
|
|
|
if (i->d_name[0] == '.')
|
|
|
|
continue;
|
|
|
|
|
|
|
|
char *e = NULL;
|
|
|
|
errno = 0;
|
|
|
|
long fd = strtol(i->d_name, &e, 10);
|
|
|
|
if (errno != 0 || !e || *e)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
if (fd == dirfd(fdDir))
|
|
|
|
continue;
|
|
|
|
|
|
|
|
if (fd < 3)
|
|
|
|
continue;
|
|
|
|
|
2019-11-12 03:50:33 -06:00
|
|
|
if (mapFdsToKeep && mapFdsToKeep->find(fd) != mapFdsToKeep->end())
|
|
|
|
continue;
|
|
|
|
|
2018-01-29 09:13:54 -06:00
|
|
|
if (close(fd) < 0)
|
2018-01-31 13:31:44 -06:00
|
|
|
std::cerr << "Unexpected failure to close fd " << fd << std::endl;
|
2018-01-29 09:13:54 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
closedir(fdDir);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2019-11-12 03:50:33 -06:00
|
|
|
static void closeFds(std::map<int, int> *mapFdsToKeep = nullptr)
|
2018-01-29 09:13:54 -06:00
|
|
|
{
|
2019-11-12 03:50:33 -06:00
|
|
|
if (!closeFdsFromProc(mapFdsToKeep))
|
2018-01-29 09:13:54 -06:00
|
|
|
{
|
2018-01-31 15:51:47 -06:00
|
|
|
std::cerr << "Couldn't close fds efficiently from /proc" << std::endl;
|
2018-01-29 09:13:54 -06:00
|
|
|
for (int fd = 3; fd < sysconf(_SC_OPEN_MAX); ++fd)
|
2019-11-12 03:50:33 -06:00
|
|
|
if (mapFdsToKeep->find(fd) != mapFdsToKeep->end())
|
|
|
|
close(fd);
|
2018-01-29 09:13:54 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-28 03:50:58 -06:00
|
|
|
int spawnProcess(const std::string &cmd, const StringVector &args, const std::vector<int>* fdsToKeep, int *stdInput)
|
2018-01-29 09:13:54 -06:00
|
|
|
{
|
|
|
|
int pipeFds[2] = { -1, -1 };
|
|
|
|
if (stdInput)
|
|
|
|
{
|
2019-11-12 03:50:33 -06:00
|
|
|
if (pipe2(pipeFds, O_NONBLOCK) < 0)
|
2018-01-29 09:13:54 -06:00
|
|
|
{
|
|
|
|
LOG_ERR("Out of file descriptors spawning " << cmd);
|
|
|
|
throw Poco::SystemException("Out of file descriptors");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-28 07:51:22 -06:00
|
|
|
// Create a vector of zero-terminated strings.
|
|
|
|
std::vector<std::string> argStrings(args.size());
|
|
|
|
for (const auto& arg : args)
|
|
|
|
{
|
|
|
|
argStrings.emplace_back(args.getParam(arg));
|
|
|
|
}
|
|
|
|
|
2018-01-29 09:13:54 -06:00
|
|
|
std::vector<char *> params;
|
|
|
|
params.push_back(const_cast<char *>(cmd.c_str()));
|
2020-02-28 07:51:22 -06:00
|
|
|
for (const auto& i : argStrings)
|
2018-01-29 09:13:54 -06:00
|
|
|
params.push_back(const_cast<char *>(i.c_str()));
|
|
|
|
params.push_back(nullptr);
|
|
|
|
|
2019-11-12 03:50:33 -06:00
|
|
|
std::map<int, int> mapFdsToKeep;
|
|
|
|
|
|
|
|
if (fdsToKeep)
|
|
|
|
for (const auto& i : *fdsToKeep)
|
|
|
|
mapFdsToKeep[i] = i;
|
|
|
|
|
2018-01-29 09:13:54 -06:00
|
|
|
int pid = fork();
|
|
|
|
if (pid < 0)
|
|
|
|
{
|
|
|
|
LOG_ERR("Failed to fork for command '" << cmd);
|
|
|
|
throw Poco::SystemException("Failed to fork for command ", cmd);
|
|
|
|
}
|
|
|
|
else if (pid == 0) // child
|
|
|
|
{
|
|
|
|
if (stdInput)
|
|
|
|
dup2(pipeFds[0], STDIN_FILENO);
|
|
|
|
|
2019-11-12 03:50:33 -06:00
|
|
|
closeFds(&mapFdsToKeep);
|
2018-01-29 09:13:54 -06:00
|
|
|
|
|
|
|
int ret = execvp(params[0], ¶ms[0]);
|
|
|
|
if (ret < 0)
|
|
|
|
std::cerr << "Failed to exec command '" << cmd << "' with error '" << strerror(errno) << "'\n";
|
2018-07-16 20:42:17 -05:00
|
|
|
Log::shutdown();
|
2018-01-29 09:13:54 -06:00
|
|
|
_exit(42);
|
|
|
|
}
|
|
|
|
// else spawning process still
|
|
|
|
if (stdInput)
|
|
|
|
{
|
|
|
|
close(pipeFds[0]);
|
|
|
|
*stdInput = pipeFds[1];
|
|
|
|
}
|
|
|
|
return pid;
|
|
|
|
}
|
2019-11-12 03:50:33 -06:00
|
|
|
|
2018-09-11 01:30:55 -05:00
|
|
|
#endif
|
2018-01-29 09:13:54 -06:00
|
|
|
|
2017-05-24 14:37:20 -05:00
|
|
|
bool dataFromHexString(const std::string& hexString, std::vector<unsigned char>& data)
|
|
|
|
{
|
|
|
|
if (hexString.length() % 2 != 0)
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
data.clear();
|
|
|
|
std::stringstream stream;
|
|
|
|
unsigned value;
|
2017-11-06 02:14:30 -06:00
|
|
|
for (unsigned long offset = 0; offset < hexString.size(); offset += 2)
|
2017-05-24 14:37:20 -05:00
|
|
|
{
|
|
|
|
stream.clear();
|
|
|
|
stream << std::hex << hexString.substr(offset, 2);
|
|
|
|
stream >> value;
|
|
|
|
data.push_back(static_cast<unsigned char>(value));
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2019-04-14 11:21:19 -05:00
|
|
|
std::string encodeId(const std::uint64_t number, const int padding)
|
2015-12-27 21:47:39 -06:00
|
|
|
{
|
|
|
|
std::ostringstream oss;
|
|
|
|
oss << std::hex << std::setw(padding) << std::setfill('0') << number;
|
|
|
|
return oss.str();
|
|
|
|
}
|
|
|
|
|
2019-04-14 11:21:19 -05:00
|
|
|
std::uint64_t decodeId(const std::string& str)
|
2016-01-09 14:46:08 -06:00
|
|
|
{
|
2019-04-14 11:21:19 -05:00
|
|
|
std::uint64_t id = 0;
|
2016-01-09 14:46:08 -06:00
|
|
|
std::stringstream ss;
|
|
|
|
ss << std::hex << str;
|
|
|
|
ss >> id;
|
|
|
|
return id;
|
|
|
|
}
|
|
|
|
|
2015-03-28 06:53:44 -05:00
|
|
|
bool windowingAvailable()
|
|
|
|
{
|
2015-12-29 19:34:53 -06:00
|
|
|
return std::getenv("DISPLAY") != nullptr;
|
2015-03-28 06:53:44 -05:00
|
|
|
}
|
|
|
|
|
2019-02-12 05:16:40 -06:00
|
|
|
#if !MOBILEAPP
|
Start on a gtk+-based workalike to the iOS app
The idea is that it would work sufficiently identically, so that even
people without a Mac and without an iOS device could participate in
development of the non-iOS-specific bits, like the JavaScript, or the
online MOBILEAPP-specific plumbing. Which would be great.
No, this doesn't do anything sane yet. It does compile the same online
C++ files as the iOS app, though. (Some minor tweaks were needed in a
couple of them to silence gcc warnings.)
There is a plain Makefile, but I should change to using autofoo, too.
Eventually, this will need to be built in a separate tree from a
normal online, just like when using the --enable-iosapp configure
switch. (But for now, doesn't matter.)
Change-Id: I13e4d921acb99d802d2f9da4b0df4a237ca60ad6
2018-10-16 16:40:49 -05:00
|
|
|
|
2017-02-03 00:29:53 -06:00
|
|
|
static const char *startsWith(const char *line, const char *tag)
|
|
|
|
{
|
2020-11-15 11:03:45 -06:00
|
|
|
std::size_t len = std::strlen(tag);
|
2017-02-03 00:29:53 -06:00
|
|
|
if (!strncmp(line, tag, len))
|
|
|
|
{
|
|
|
|
while (!isdigit(line[len]) && line[len] != '\0')
|
|
|
|
++len;
|
|
|
|
|
|
|
|
return line + len;
|
|
|
|
}
|
|
|
|
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2018-01-26 08:32:09 -06:00
|
|
|
std::string getHumanizedBytes(unsigned long nBytes)
|
|
|
|
{
|
|
|
|
constexpr unsigned factor = 1024;
|
|
|
|
short count = 0;
|
|
|
|
float val = nBytes;
|
|
|
|
while (val >= factor && count < 4) {
|
|
|
|
val /= factor;
|
|
|
|
count++;
|
|
|
|
}
|
|
|
|
std::string unit;
|
|
|
|
switch (count)
|
|
|
|
{
|
|
|
|
case 0: unit = ""; break;
|
|
|
|
case 1: unit = "ki"; break;
|
|
|
|
case 2: unit = "Mi"; break;
|
|
|
|
case 3: unit = "Gi"; break;
|
|
|
|
case 4: unit = "Ti"; break;
|
|
|
|
default: assert(false);
|
|
|
|
}
|
|
|
|
|
2020-05-24 08:10:18 -05:00
|
|
|
unit += 'B';
|
2018-01-26 08:32:09 -06:00
|
|
|
std::stringstream ss;
|
|
|
|
ss << std::fixed << std::setprecision(1) << val << ' ' << unit;
|
|
|
|
return ss.str();
|
|
|
|
}
|
|
|
|
|
2020-11-15 11:03:45 -06:00
|
|
|
std::size_t getTotalSystemMemoryKb()
|
2017-07-07 06:42:19 -05:00
|
|
|
{
|
2020-11-15 11:03:45 -06:00
|
|
|
std::size_t totalMemKb = 0;
|
2017-07-07 06:42:19 -05:00
|
|
|
FILE* file = fopen("/proc/meminfo", "r");
|
|
|
|
if (file != nullptr)
|
|
|
|
{
|
|
|
|
char line[4096] = { 0 };
|
|
|
|
while (fgets(line, sizeof(line), file))
|
|
|
|
{
|
|
|
|
const char* value;
|
|
|
|
if ((value = startsWith(line, "MemTotal:")))
|
|
|
|
{
|
|
|
|
totalMemKb = atoi(value);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return totalMemKb;
|
|
|
|
}
|
|
|
|
|
2020-11-15 11:03:45 -06:00
|
|
|
std::pair<std::size_t, std::size_t> getPssAndDirtyFromSMaps(FILE* file)
|
2017-02-03 00:29:53 -06:00
|
|
|
{
|
2020-11-15 11:03:45 -06:00
|
|
|
std::size_t numPSSKb = 0;
|
|
|
|
std::size_t numDirtyKb = 0;
|
2017-02-03 00:29:53 -06:00
|
|
|
if (file)
|
|
|
|
{
|
|
|
|
rewind(file);
|
|
|
|
char line[4096] = { 0 };
|
|
|
|
while (fgets(line, sizeof (line), file))
|
|
|
|
{
|
|
|
|
const char *value;
|
2017-02-07 08:39:56 -06:00
|
|
|
// Shared_Dirty is accounted for by forkit's RSS
|
2017-02-07 14:07:37 -06:00
|
|
|
if ((value = startsWith(line, "Private_Dirty:")))
|
2017-02-03 00:29:53 -06:00
|
|
|
{
|
|
|
|
numDirtyKb += atoi(value);
|
|
|
|
}
|
|
|
|
else if ((value = startsWith(line, "Pss:")))
|
|
|
|
{
|
|
|
|
numPSSKb += atoi(value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return std::make_pair(numPSSKb, numDirtyKb);
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string getMemoryStats(FILE* file)
|
|
|
|
{
|
2020-11-15 11:03:45 -06:00
|
|
|
const std::pair<std::size_t, std::size_t> pssAndDirtyKb = getPssAndDirtyFromSMaps(file);
|
2017-02-03 00:29:53 -06:00
|
|
|
std::ostringstream oss;
|
|
|
|
oss << "procmemstats: pid=" << getpid()
|
|
|
|
<< " pss=" << pssAndDirtyKb.first
|
|
|
|
<< " dirty=" << pssAndDirtyKb.second;
|
|
|
|
LOG_TRC("Collected " << oss.str());
|
|
|
|
return oss.str();
|
|
|
|
}
|
|
|
|
|
2020-11-15 11:03:45 -06:00
|
|
|
std::size_t getMemoryUsagePSS(const pid_t pid)
|
2017-02-03 00:29:53 -06:00
|
|
|
{
|
|
|
|
if (pid > 0)
|
|
|
|
{
|
|
|
|
const auto cmd = "/proc/" + std::to_string(pid) + "/smaps";
|
|
|
|
FILE* fp = fopen(cmd.c_str(), "r");
|
|
|
|
if (fp != nullptr)
|
|
|
|
{
|
2020-11-15 11:03:45 -06:00
|
|
|
const std::size_t pss = getPssAndDirtyFromSMaps(fp).first;
|
2017-02-03 00:29:53 -06:00
|
|
|
fclose(fp);
|
|
|
|
return pss;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2020-11-15 11:03:45 -06:00
|
|
|
std::size_t getMemoryUsageRSS(const pid_t pid)
|
2016-03-04 12:49:01 -06:00
|
|
|
{
|
2018-02-07 03:17:19 -06:00
|
|
|
static const int pageSizeBytes = getpagesize();
|
2020-11-15 11:03:45 -06:00
|
|
|
std::size_t rss = 0;
|
2017-01-15 12:32:07 -06:00
|
|
|
|
2017-06-05 20:17:42 -05:00
|
|
|
if (pid > 0)
|
|
|
|
{
|
|
|
|
rss = getStatFromPid(pid, 23);
|
|
|
|
rss *= pageSizeBytes;
|
|
|
|
rss /= 1024;
|
|
|
|
return rss;
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2020-11-15 11:03:45 -06:00
|
|
|
std::size_t getCpuUsage(const pid_t pid)
|
2017-06-05 20:17:42 -05:00
|
|
|
{
|
|
|
|
if (pid > 0)
|
|
|
|
{
|
2020-11-15 11:03:45 -06:00
|
|
|
std::size_t totalJiffies = 0;
|
2017-06-05 20:17:42 -05:00
|
|
|
totalJiffies += getStatFromPid(pid, 13);
|
|
|
|
totalJiffies += getStatFromPid(pid, 14);
|
|
|
|
return totalJiffies;
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2020-11-15 11:03:45 -06:00
|
|
|
std::size_t getStatFromPid(const pid_t pid, int ind)
|
2017-06-05 20:17:42 -05:00
|
|
|
{
|
2017-02-08 22:30:28 -06:00
|
|
|
if (pid > 0)
|
2016-03-04 12:49:01 -06:00
|
|
|
{
|
2017-02-08 22:30:28 -06:00
|
|
|
const auto cmd = "/proc/" + std::to_string(pid) + "/stat";
|
|
|
|
FILE* fp = fopen(cmd.c_str(), "r");
|
|
|
|
if (fp != nullptr)
|
2016-11-09 20:12:09 -06:00
|
|
|
{
|
2017-02-08 22:30:28 -06:00
|
|
|
char line[4096] = { 0 };
|
|
|
|
if (fgets(line, sizeof (line), fp))
|
|
|
|
{
|
|
|
|
const std::string s(line);
|
|
|
|
int index = 1;
|
2020-11-15 11:03:45 -06:00
|
|
|
std::size_t pos = s.find(' ');
|
2017-02-08 22:30:28 -06:00
|
|
|
while (pos != std::string::npos)
|
|
|
|
{
|
2017-06-05 20:17:42 -05:00
|
|
|
if (index == ind)
|
2017-02-08 22:30:28 -06:00
|
|
|
{
|
2017-06-05 20:17:42 -05:00
|
|
|
fclose(fp);
|
|
|
|
return strtol(&s[pos], nullptr, 10);
|
2017-02-08 22:30:28 -06:00
|
|
|
}
|
|
|
|
++index;
|
|
|
|
pos = s.find(' ', pos + 1);
|
|
|
|
}
|
|
|
|
}
|
2016-11-09 20:12:09 -06:00
|
|
|
}
|
2016-03-04 12:49:01 -06:00
|
|
|
}
|
2017-01-15 12:32:07 -06:00
|
|
|
return 0;
|
2016-03-04 12:49:01 -06:00
|
|
|
}
|
2020-05-02 13:13:14 -05:00
|
|
|
|
|
|
|
void setProcessAndThreadPriorities(const pid_t pid, int prio)
|
|
|
|
{
|
|
|
|
int res = setpriority(PRIO_PROCESS, pid, prio);
|
|
|
|
LOG_TRC("Lowered kit [" << (int)pid << "] priority: " << prio << " with result: " << res);
|
|
|
|
|
2020-11-08 09:36:41 -06:00
|
|
|
#ifdef __linux
|
2020-05-02 13:13:14 -05:00
|
|
|
// rely on Linux thread-id priority setting to drop this thread' priority
|
|
|
|
pid_t tid = getThreadId();
|
|
|
|
res = setpriority(PRIO_PROCESS, tid, prio);
|
|
|
|
LOG_TRC("Lowered own thread [" << (int)tid << "] priority: " << prio << " with result: " << res);
|
2020-11-08 09:36:41 -06:00
|
|
|
#endif
|
2020-05-02 13:13:14 -05:00
|
|
|
}
|
|
|
|
|
2018-09-11 01:30:55 -05:00
|
|
|
#endif
|
2016-03-31 03:01:21 -05:00
|
|
|
|
2017-05-14 20:44:16 -05:00
|
|
|
std::string replace(std::string result, const std::string& a, const std::string& b)
|
2016-03-31 03:01:21 -05:00
|
|
|
{
|
2020-11-15 11:03:45 -06:00
|
|
|
const std::size_t aSize = a.size();
|
2017-05-14 20:44:16 -05:00
|
|
|
if (aSize > 0)
|
2016-03-31 03:01:21 -05:00
|
|
|
{
|
2020-11-15 11:03:45 -06:00
|
|
|
const std::size_t bSize = b.size();
|
2017-05-14 20:44:16 -05:00
|
|
|
std::string::size_type pos = 0;
|
|
|
|
while ((pos = result.find(a, pos)) != std::string::npos)
|
|
|
|
{
|
|
|
|
result = result.replace(pos, aSize, b);
|
|
|
|
pos += bSize; // Skip the replacee to avoid endless recursion.
|
|
|
|
}
|
2016-03-31 03:01:21 -05:00
|
|
|
}
|
2017-05-14 20:44:16 -05:00
|
|
|
|
2016-03-31 03:01:21 -05:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string formatLinesForLog(const std::string& s)
|
|
|
|
{
|
|
|
|
std::string r;
|
|
|
|
std::string::size_type n = s.size();
|
|
|
|
if (n > 0 && s.back() == '\n')
|
|
|
|
r = s.substr(0, n-1);
|
|
|
|
else
|
|
|
|
r = s;
|
|
|
|
return replace(r, "\n", " / ");
|
|
|
|
}
|
2016-04-07 02:55:57 -05:00
|
|
|
|
2019-09-21 13:38:04 -05:00
|
|
|
// prctl(2) supports names of up to 16 characters, including null-termination.
|
|
|
|
// Although in practice on linux more than 16 chars is supported.
|
2019-04-20 14:23:32 -05:00
|
|
|
static thread_local char ThreadName[32] = {0};
|
2019-09-21 13:38:04 -05:00
|
|
|
static_assert(sizeof(ThreadName) >= 16, "ThreadName should have a statically known size, and not be a pointer.");
|
2017-03-30 12:14:40 -05:00
|
|
|
|
2016-04-07 02:55:57 -05:00
|
|
|
void setThreadName(const std::string& s)
|
|
|
|
{
|
2019-09-21 13:38:04 -05:00
|
|
|
// Copy the current name.
|
|
|
|
const std::string knownAs
|
2020-05-24 08:10:18 -05:00
|
|
|
= ThreadName[0] ? "known as [" + std::string(ThreadName) + ']' : "unnamed";
|
2019-09-21 13:38:04 -05:00
|
|
|
|
|
|
|
// Set the new name.
|
|
|
|
strncpy(ThreadName, s.c_str(), sizeof(ThreadName) - 1);
|
|
|
|
ThreadName[sizeof(ThreadName) - 1] = '\0';
|
2018-08-29 10:47:32 -05:00
|
|
|
#ifdef __linux
|
2016-04-07 02:55:57 -05:00
|
|
|
if (prctl(PR_SET_NAME, reinterpret_cast<unsigned long>(s.c_str()), 0, 0, 0) != 0)
|
2019-09-21 13:38:04 -05:00
|
|
|
LOG_SYS("Cannot set thread name of "
|
|
|
|
<< getThreadId() << " (" << std::hex << std::this_thread::get_id() << std::dec
|
|
|
|
<< ") of process " << getpid() << " currently " << knownAs << " to [" << s
|
|
|
|
<< "].");
|
2017-03-18 21:19:41 -05:00
|
|
|
else
|
2019-09-21 13:38:04 -05:00
|
|
|
LOG_INF("Thread " << getThreadId() << " (" << std::hex << std::this_thread::get_id()
|
|
|
|
<< std::dec << ") of process " << getpid() << " formerly " << knownAs
|
|
|
|
<< " is now called [" << s << "].");
|
2018-09-05 07:11:05 -05:00
|
|
|
#elif defined IOS
|
2018-08-29 10:47:32 -05:00
|
|
|
[[NSThread currentThread] setName:[NSString stringWithUTF8String:ThreadName]];
|
2018-10-16 08:51:05 -05:00
|
|
|
LOG_INF("Thread " << getThreadId() <<
|
2018-08-29 10:47:32 -05:00
|
|
|
") is now called [" << s << "].");
|
|
|
|
#endif
|
2016-04-07 02:55:57 -05:00
|
|
|
}
|
2016-04-15 09:07:24 -05:00
|
|
|
|
2017-03-30 12:14:40 -05:00
|
|
|
const char *getThreadName()
|
|
|
|
{
|
2017-03-31 05:21:35 -05:00
|
|
|
// Main process and/or not set yet.
|
|
|
|
if (ThreadName[0] == '\0')
|
|
|
|
{
|
2018-08-29 10:47:32 -05:00
|
|
|
#ifdef __linux
|
2019-09-21 13:38:04 -05:00
|
|
|
// prctl(2): The buffer should allow space for up to 16 bytes; the returned string will be null-terminated.
|
2017-03-31 05:21:35 -05:00
|
|
|
if (prctl(PR_GET_NAME, reinterpret_cast<unsigned long>(ThreadName), 0, 0, 0) != 0)
|
2018-06-04 15:47:39 -05:00
|
|
|
strncpy(ThreadName, "<noid>", sizeof(ThreadName) - 1);
|
2018-09-05 07:11:05 -05:00
|
|
|
#elif defined IOS
|
2018-08-29 10:47:32 -05:00
|
|
|
const char *const name = [[[NSThread currentThread] name] UTF8String];
|
2019-09-21 13:38:04 -05:00
|
|
|
strncpy(ThreadName, name, sizeof(ThreadName) - 1);
|
2018-08-29 10:47:32 -05:00
|
|
|
#endif
|
2019-09-21 13:38:04 -05:00
|
|
|
ThreadName[sizeof(ThreadName) - 1] = '\0';
|
2017-03-31 05:21:35 -05:00
|
|
|
}
|
|
|
|
|
2017-03-30 12:14:40 -05:00
|
|
|
// Avoid so many redundant system calls
|
|
|
|
return ThreadName;
|
|
|
|
}
|
|
|
|
|
2018-08-29 10:47:32 -05:00
|
|
|
#ifdef __linux
|
2019-04-20 14:23:32 -05:00
|
|
|
static thread_local pid_t ThreadTid = 0;
|
2017-03-30 12:14:40 -05:00
|
|
|
|
|
|
|
pid_t getThreadId()
|
2018-08-29 10:47:32 -05:00
|
|
|
#else
|
|
|
|
std::thread::id getThreadId()
|
|
|
|
#endif
|
2017-03-30 12:14:40 -05:00
|
|
|
{
|
|
|
|
// Avoid so many redundant system calls
|
2018-08-29 10:47:32 -05:00
|
|
|
#ifdef __linux
|
2017-03-30 12:14:40 -05:00
|
|
|
if (!ThreadTid)
|
2018-06-04 15:47:39 -05:00
|
|
|
ThreadTid = ::syscall(SYS_gettid);
|
2017-03-30 12:14:40 -05:00
|
|
|
return ThreadTid;
|
2018-08-29 10:47:32 -05:00
|
|
|
#else
|
|
|
|
return std::this_thread::get_id();
|
|
|
|
#endif
|
2017-03-30 12:14:40 -05:00
|
|
|
}
|
|
|
|
|
2016-06-20 04:51:35 -05:00
|
|
|
void getVersionInfo(std::string& version, std::string& hash)
|
2016-04-15 09:07:24 -05:00
|
|
|
{
|
2016-06-20 04:51:35 -05:00
|
|
|
version = std::string(LOOLWSD_VERSION);
|
|
|
|
hash = std::string(LOOLWSD_VERSION_HASH);
|
2016-04-15 09:07:24 -05:00
|
|
|
hash.resize(std::min(8, (int)hash.length()));
|
|
|
|
}
|
2016-04-20 08:43:00 -05:00
|
|
|
|
2020-07-02 07:36:50 -05:00
|
|
|
std::string getProcessIdentifier()
|
|
|
|
{
|
|
|
|
static std::string id = Util::rng::getHexString(8);
|
|
|
|
|
|
|
|
return id;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string getVersionJSON()
|
|
|
|
{
|
|
|
|
std::string version, hash;
|
|
|
|
Util::getVersionInfo(version, hash);
|
|
|
|
return
|
|
|
|
"{ \"Version\": \"" + version + "\", "
|
|
|
|
"\"Hash\": \"" + hash + "\", "
|
|
|
|
"\"Protocol\": \"" + LOOLProtocol::GetProtocolVersion() + "\", "
|
|
|
|
"\"Id\": \"" + Util::getProcessIdentifier() + "\" }";
|
|
|
|
}
|
|
|
|
|
2016-04-20 08:43:00 -05:00
|
|
|
std::string UniqueId()
|
|
|
|
{
|
|
|
|
static std::atomic_int counter(0);
|
2019-11-07 04:53:37 -06:00
|
|
|
return std::to_string(getpid()) + '/' + std::to_string(counter++);
|
2017-05-28 11:20:49 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
std::map<std::string, std::string> JsonToMap(const std::string& jsonString)
|
|
|
|
{
|
2019-03-09 20:21:48 -06:00
|
|
|
std::map<std::string, std::string> map;
|
|
|
|
if (jsonString.empty())
|
|
|
|
return map;
|
|
|
|
|
2017-05-28 11:20:49 -05:00
|
|
|
Poco::JSON::Parser parser;
|
2018-02-07 03:17:19 -06:00
|
|
|
const Poco::Dynamic::Var result = parser.parse(jsonString);
|
2017-05-28 11:20:49 -05:00
|
|
|
const auto& json = result.extract<Poco::JSON::Object::Ptr>();
|
|
|
|
|
|
|
|
std::vector<std::string> names;
|
|
|
|
json->getNames(names);
|
|
|
|
|
|
|
|
for (const auto& name : names)
|
|
|
|
{
|
|
|
|
map[name] = json->get(name).toString();
|
|
|
|
}
|
|
|
|
|
|
|
|
return map;
|
2016-04-20 08:43:00 -05:00
|
|
|
}
|
2018-04-04 05:36:11 -05:00
|
|
|
|
|
|
|
bool isValidURIScheme(const std::string& scheme)
|
|
|
|
{
|
|
|
|
if (scheme.empty())
|
|
|
|
return false;
|
|
|
|
|
|
|
|
for (char c : scheme)
|
|
|
|
{
|
|
|
|
if (!isalpha(c))
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool isValidURIHost(const std::string& host)
|
|
|
|
{
|
|
|
|
if (host.empty())
|
|
|
|
return false;
|
|
|
|
|
|
|
|
for (char c : host)
|
|
|
|
{
|
|
|
|
if (!isalnum(c) && c != '_' && c != '-' && c != '.' && c !=':' && c != '[' && c != ']')
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
2018-06-10 10:35:59 -05:00
|
|
|
|
2019-10-08 04:23:29 -05:00
|
|
|
/// Split a string in two at the delimiter and give the delimiter to the first.
|
2018-07-17 01:01:05 -05:00
|
|
|
static
|
2019-10-08 04:23:29 -05:00
|
|
|
std::pair<std::string, std::string> splitLast2(const char* s, const int length, const char delimiter = ' ')
|
2018-07-17 01:01:05 -05:00
|
|
|
{
|
|
|
|
if (s != nullptr && length > 0)
|
|
|
|
{
|
2019-10-08 04:23:29 -05:00
|
|
|
const int pos = getLastDelimiterPosition(s, length, delimiter);
|
2018-07-17 01:01:05 -05:00
|
|
|
if (pos < length)
|
|
|
|
return std::make_pair(std::string(s, pos + 1), std::string(s + pos + 1));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Not found; return in first.
|
|
|
|
return std::make_pair(std::string(s, length), std::string());
|
|
|
|
}
|
|
|
|
|
|
|
|
std::tuple<std::string, std::string, std::string, std::string> splitUrl(const std::string& url)
|
|
|
|
{
|
|
|
|
// In case we have a URL that has parameters.
|
|
|
|
std::string base;
|
|
|
|
std::string params;
|
|
|
|
std::tie(base, params) = Util::split(url, '?', false);
|
|
|
|
|
|
|
|
std::string filename;
|
|
|
|
std::tie(base, filename) = Util::splitLast2(base.c_str(), base.size(), '/');
|
|
|
|
if (filename.empty())
|
|
|
|
{
|
|
|
|
// If no '/', then it's only filename.
|
|
|
|
std::swap(base, filename);
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string ext;
|
|
|
|
std::tie(filename, ext) = Util::splitLast(filename, '.', false);
|
|
|
|
|
|
|
|
return std::make_tuple(base, filename, ext, params);
|
|
|
|
}
|
|
|
|
|
2019-04-14 13:14:12 -05:00
|
|
|
static std::unordered_map<std::string, std::string> AnonymizedStrings;
|
2019-04-14 11:21:19 -05:00
|
|
|
static std::atomic<unsigned> AnonymizationCounter(0);
|
2018-07-10 22:09:27 -05:00
|
|
|
static std::mutex AnonymizedMutex;
|
|
|
|
|
|
|
|
void mapAnonymized(const std::string& plain, const std::string& anonymized)
|
|
|
|
{
|
2018-07-12 17:00:33 -05:00
|
|
|
if (plain.empty() || anonymized.empty())
|
|
|
|
return;
|
|
|
|
|
2019-04-14 13:14:12 -05:00
|
|
|
if (Log::traceEnabled() && plain != anonymized)
|
2019-03-15 04:51:19 -05:00
|
|
|
LOG_TRC("Anonymizing [" << plain << "] -> [" << anonymized << "].");
|
2018-07-10 22:09:27 -05:00
|
|
|
|
|
|
|
std::unique_lock<std::mutex> lock(AnonymizedMutex);
|
|
|
|
|
|
|
|
AnonymizedStrings[plain] = anonymized;
|
|
|
|
}
|
2018-06-10 10:35:59 -05:00
|
|
|
|
2019-04-14 11:21:19 -05:00
|
|
|
std::string anonymize(const std::string& text, const std::uint64_t nAnonymizationSalt)
|
2018-06-10 10:35:59 -05:00
|
|
|
{
|
2018-07-10 22:09:27 -05:00
|
|
|
{
|
|
|
|
std::unique_lock<std::mutex> lock(AnonymizedMutex);
|
|
|
|
|
|
|
|
const auto it = AnonymizedStrings.find(text);
|
|
|
|
if (it != AnonymizedStrings.end())
|
2018-07-12 17:00:33 -05:00
|
|
|
{
|
2019-04-14 13:14:12 -05:00
|
|
|
if (Log::traceEnabled() && text != it->second)
|
2019-03-15 04:51:19 -05:00
|
|
|
LOG_TRC("Found anonymized [" << text << "] -> [" << it->second << "].");
|
2018-07-10 22:09:27 -05:00
|
|
|
return it->second;
|
2018-07-12 17:00:33 -05:00
|
|
|
}
|
2018-07-10 22:09:27 -05:00
|
|
|
}
|
2018-06-10 10:35:59 -05:00
|
|
|
|
2019-04-14 11:21:19 -05:00
|
|
|
// Modified 64-bit FNV-1a to add salting.
|
|
|
|
// For the algorithm and the magic numbers, see http://isthe.com/chongo/tech/comp/fnv/
|
|
|
|
std::uint64_t hash = 0xCBF29CE484222325LL;
|
|
|
|
hash ^= nAnonymizationSalt;
|
|
|
|
hash *= 0x100000001b3ULL;
|
2018-06-10 10:35:59 -05:00
|
|
|
for (const char c : text)
|
2019-04-14 11:21:19 -05:00
|
|
|
{
|
|
|
|
hash ^= static_cast<std::uint64_t>(c);
|
|
|
|
hash *= 0x100000001b3ULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
hash ^= nAnonymizationSalt;
|
|
|
|
hash *= 0x100000001b3ULL;
|
2018-06-10 10:35:59 -05:00
|
|
|
|
|
|
|
// Generate the anonymized string. The '#' is to hint that it's anonymized.
|
2019-04-14 11:21:19 -05:00
|
|
|
// Prepend with count to make it unique within a single process instance,
|
|
|
|
// in case we get collisions (which we will, eventually). N.B.: Identical
|
|
|
|
// strings likely to have different prefixes when logged in WSD process vs. Kit.
|
|
|
|
const std::string res
|
|
|
|
= '#' + Util::encodeId(AnonymizationCounter++, 0) + '#' + Util::encodeId(hash, 0) + '#';
|
2018-07-10 22:09:27 -05:00
|
|
|
mapAnonymized(text, res);
|
2018-06-10 10:35:59 -05:00
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
2018-07-17 01:01:05 -05:00
|
|
|
std::string getFilenameFromURL(const std::string& url)
|
2018-06-10 10:35:59 -05:00
|
|
|
{
|
2018-07-17 01:01:05 -05:00
|
|
|
std::string base;
|
|
|
|
std::string filename;
|
2018-06-10 10:35:59 -05:00
|
|
|
std::string ext;
|
2018-07-17 01:01:05 -05:00
|
|
|
std::string params;
|
|
|
|
std::tie(base, filename, ext, params) = Util::splitUrl(url);
|
|
|
|
return filename;
|
2018-07-10 22:09:27 -05:00
|
|
|
}
|
|
|
|
|
2019-04-14 11:21:19 -05:00
|
|
|
std::string anonymizeUrl(const std::string& url, const std::uint64_t nAnonymizationSalt)
|
2018-06-10 10:35:59 -05:00
|
|
|
{
|
2018-07-17 01:01:05 -05:00
|
|
|
std::string base;
|
|
|
|
std::string filename;
|
|
|
|
std::string ext;
|
|
|
|
std::string params;
|
|
|
|
std::tie(base, filename, ext, params) = Util::splitUrl(url);
|
2018-06-10 10:35:59 -05:00
|
|
|
|
2019-04-14 11:21:19 -05:00
|
|
|
return base + Util::anonymize(filename, nAnonymizationSalt) + ext + params;
|
2018-06-10 10:35:59 -05:00
|
|
|
}
|
2019-05-20 00:40:12 -05:00
|
|
|
|
|
|
|
std::string getHttpTimeNow()
|
|
|
|
{
|
2019-08-30 12:37:55 -05:00
|
|
|
char time_now[64];
|
2019-05-20 00:40:12 -05:00
|
|
|
std::chrono::system_clock::time_point now = std::chrono::system_clock::now();
|
|
|
|
std::time_t now_c = std::chrono::system_clock::to_time_t(now);
|
|
|
|
std::tm now_tm = *std::gmtime(&now_c);
|
2019-08-30 12:37:55 -05:00
|
|
|
strftime(time_now, sizeof(time_now), "%a, %d %b %Y %T", &now_tm);
|
2019-05-20 00:40:12 -05:00
|
|
|
|
|
|
|
return time_now;
|
|
|
|
}
|
2019-05-29 10:26:16 -05:00
|
|
|
|
2019-09-27 11:56:16 -05:00
|
|
|
std::string getHttpTime(std::chrono::system_clock::time_point time)
|
|
|
|
{
|
|
|
|
char http_time[64];
|
|
|
|
std::time_t time_c = std::chrono::system_clock::to_time_t(time);
|
|
|
|
std::tm time_tm = *std::gmtime(&time_c);
|
|
|
|
strftime(http_time, sizeof(http_time), "%a, %d %b %Y %T", &time_tm);
|
|
|
|
|
|
|
|
return http_time;
|
|
|
|
}
|
|
|
|
|
2020-11-15 11:03:45 -06:00
|
|
|
std::size_t findInVector(const std::vector<char>& tokens, const char *cstring)
|
2019-05-29 10:26:16 -05:00
|
|
|
{
|
|
|
|
assert(cstring);
|
2020-11-15 11:03:45 -06:00
|
|
|
for (std::size_t i = 0; i < tokens.size(); ++i)
|
2019-05-29 10:26:16 -05:00
|
|
|
{
|
2020-11-15 11:03:45 -06:00
|
|
|
std::size_t j;
|
2019-05-29 10:26:16 -05:00
|
|
|
for (j = 0; i + j < tokens.size() && cstring[j] != '\0' && tokens[i + j] == cstring[j]; ++j)
|
|
|
|
;
|
|
|
|
if (cstring[j] == '\0')
|
|
|
|
return i;
|
|
|
|
}
|
|
|
|
return std::string::npos;
|
|
|
|
}
|
2019-08-30 12:37:55 -05:00
|
|
|
|
|
|
|
std::string getIso8601FracformatTime(std::chrono::system_clock::time_point time){
|
|
|
|
char time_modified[64];
|
2019-09-03 05:23:32 -05:00
|
|
|
std::time_t lastModified_us_t = std::chrono::system_clock::to_time_t(time);
|
2019-08-30 12:37:55 -05:00
|
|
|
std::tm lastModified_tm = *std::gmtime(&lastModified_us_t);
|
|
|
|
strftime(time_modified, sizeof(time_modified), "%FT%T.", &lastModified_tm);
|
|
|
|
|
|
|
|
auto lastModified_s = std::chrono::time_point_cast<std::chrono::seconds>(time);
|
|
|
|
|
|
|
|
std::ostringstream oss;
|
|
|
|
oss << std::setfill('0')
|
|
|
|
<< time_modified
|
|
|
|
<< std::setw(6)
|
2020-10-11 15:49:02 -05:00
|
|
|
<< (time - lastModified_s).count() / (std::chrono::system_clock::period::den / std::chrono::system_clock::period::num / 1000000)
|
2020-05-24 08:10:18 -05:00
|
|
|
<< 'Z';
|
2019-08-30 12:37:55 -05:00
|
|
|
|
|
|
|
return oss.str();
|
|
|
|
}
|
|
|
|
|
2019-10-25 19:48:05 -05:00
|
|
|
std::string time_point_to_iso8601(std::chrono::system_clock::time_point tp)
|
|
|
|
{
|
|
|
|
const std::time_t tt = std::chrono::system_clock::to_time_t(tp);
|
|
|
|
std::tm tm;
|
|
|
|
gmtime_r(&tt, &tm);
|
|
|
|
|
|
|
|
std::ostringstream oss;
|
|
|
|
oss << tm.tm_year + 1900 << '-' << std::setfill('0') << std::setw(2) << tm.tm_mon + 1 << '-'
|
|
|
|
<< std::setfill('0') << std::setw(2) << tm.tm_mday << 'T';
|
|
|
|
oss << std::setfill('0') << std::setw(2) << tm.tm_hour << ':';
|
|
|
|
oss << std::setfill('0') << std::setw(2) << tm.tm_min << ':';
|
|
|
|
const std::chrono::duration<double> sec
|
|
|
|
= tp - std::chrono::system_clock::from_time_t(tt) + std::chrono::seconds(tm.tm_sec);
|
|
|
|
if (sec.count() < 10)
|
|
|
|
oss << '0';
|
|
|
|
oss << std::fixed << sec.count() << 'Z';
|
|
|
|
|
|
|
|
return oss.str();
|
|
|
|
}
|
|
|
|
|
|
|
|
std::chrono::system_clock::time_point iso8601ToTimestamp(const std::string& iso8601Time,
|
|
|
|
const std::string& logName)
|
2019-08-30 12:37:55 -05:00
|
|
|
{
|
|
|
|
std::chrono::system_clock::time_point timestamp;
|
2019-09-05 08:07:21 -05:00
|
|
|
std::tm tm;
|
2019-10-25 19:48:05 -05:00
|
|
|
const char* cstr = iso8601Time.c_str();
|
|
|
|
const char* trailing;
|
2019-09-05 08:07:21 -05:00
|
|
|
if (!(trailing = strptime(cstr, "%Y-%m-%dT%H:%M:%S", &tm)))
|
2019-08-30 12:37:55 -05:00
|
|
|
{
|
|
|
|
LOG_WRN(logName << " [" << iso8601Time << "] is in invalid format."
|
2019-10-25 19:48:05 -05:00
|
|
|
<< "Returning " << timestamp.time_since_epoch().count());
|
2019-08-30 12:37:55 -05:00
|
|
|
return timestamp;
|
|
|
|
}
|
2019-10-25 19:48:05 -05:00
|
|
|
|
2019-08-30 12:37:55 -05:00
|
|
|
timestamp += std::chrono::seconds(timegm(&tm));
|
2019-09-05 08:07:21 -05:00
|
|
|
if (trailing[0] == '\0')
|
2019-08-30 12:37:55 -05:00
|
|
|
return timestamp;
|
2019-10-25 19:48:05 -05:00
|
|
|
|
2019-09-05 08:07:21 -05:00
|
|
|
if (trailing[0] != '.')
|
2019-08-30 12:37:55 -05:00
|
|
|
{
|
|
|
|
LOG_WRN(logName << " [" << iso8601Time << "] is in invalid format."
|
2019-10-25 19:48:05 -05:00
|
|
|
<< ". Returning " << timestamp.time_since_epoch().count());
|
2019-08-30 12:37:55 -05:00
|
|
|
return timestamp;
|
|
|
|
}
|
2019-10-25 19:48:05 -05:00
|
|
|
|
|
|
|
char* end = nullptr;
|
2020-11-15 11:03:45 -06:00
|
|
|
const std::size_t us = strtoul(trailing + 1, &end, 10); // Skip the '.' and read as integer.
|
2020-10-11 15:49:02 -05:00
|
|
|
|
|
|
|
std::size_t denominator = 1;
|
|
|
|
for (const char* i = trailing + 1; i != end; i++)
|
|
|
|
{
|
|
|
|
denominator *= 10;
|
|
|
|
}
|
|
|
|
|
2019-10-25 19:48:05 -05:00
|
|
|
const std::size_t seconds_us = us * std::chrono::system_clock::period::den
|
2020-10-11 15:49:02 -05:00
|
|
|
/ std::chrono::system_clock::period::num / denominator;
|
2019-08-30 12:37:55 -05:00
|
|
|
|
|
|
|
timestamp += std::chrono::system_clock::duration(seconds_us);
|
|
|
|
|
|
|
|
return timestamp;
|
|
|
|
}
|
2019-11-27 03:12:59 -06:00
|
|
|
|
|
|
|
std::string getSteadyClockAsString(const std::chrono::steady_clock::time_point &time)
|
|
|
|
{
|
|
|
|
auto now = std::chrono::steady_clock::now();
|
|
|
|
const std::time_t t = std::chrono::system_clock::to_time_t(
|
|
|
|
std::chrono::time_point_cast<std::chrono::seconds>(
|
|
|
|
std::chrono::system_clock::now() + (time - now)));
|
|
|
|
return std::ctime(&t);
|
|
|
|
}
|
Add an initial libfuzzer based fuzzer
- target ClientSession::_handleInput(), since crashing there would bring
down the whole loolwsd (not just a kit process), and it deals with
input from untrusted users (browsers)
- add a --enable-fuzzers configure switch to build with
-fsanitize=fuzzer (compared to normal sanitizers build, this is the only
special flag needed)
- configuring other sanitizers is not done automatically, either use
--with-sanitizer=... or the environment variables from LODE's sanitizer
config
- run the actual fuzzer like this:
./clientsession_fuzzer -max_len=16384 fuzzer/data/
- note that at least openSUSE Leap 15.1 sadly ships with a clang with
libfuzzer static libs removed from the package, so you need a
self-built clang to run the fuzzer (either manual build or one from
LODE)
- <https://chromium.googlesource.com/chromium/src/testing/libfuzzer/+/refs/heads/master/efficient_fuzzing.md#execution-speed>
suggests that "You should aim for at least 1,000 exec/s from your fuzz
target locally" (i.e. one run should not take more than 1 ms), so try
this minimal approach first. The alternative would be to start from the
existing loolwsd_fuzzer binary, then step by step cut it down to not
fork(), not do any network traffic, etc -- till it's fast enough that
the fuzzer can find interesting input
- the various configurations start to be really complex (the matrix is
just very large), so try to use Util::isFuzzing() for fuzzer-specific
changes (this is what core.git does as well), and only resort to ifdefs
for the Util::isFuzzing() itself
Change-Id: I72dc1193b34c93eacb5d8e39cef42387d42bd72f
Reviewed-on: https://gerrit.libreoffice.org/c/online/+/89226
Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice@gmail.com>
Reviewed-by: Michael Meeks <michael.meeks@collabora.com>
2020-02-21 08:52:20 -06:00
|
|
|
|
|
|
|
bool isFuzzing()
|
|
|
|
{
|
|
|
|
#if LIBFUZZER
|
|
|
|
return true;
|
|
|
|
#else
|
|
|
|
return false;
|
|
|
|
#endif
|
|
|
|
}
|
2020-03-29 18:31:23 -05:00
|
|
|
|
|
|
|
std::map<std::string, std::string> stringVectorToMap(std::vector<std::string> sVector, const char delimiter)
|
|
|
|
{
|
|
|
|
std::map<std::string, std::string> result;
|
|
|
|
|
|
|
|
for (std::vector<std::string>::iterator it = sVector.begin(); it != sVector.end(); it++)
|
|
|
|
{
|
2020-11-15 11:03:45 -06:00
|
|
|
std::size_t delimiterPosition = 0;
|
2020-03-29 18:31:23 -05:00
|
|
|
delimiterPosition = (*it).find(delimiter, 0);
|
|
|
|
if (delimiterPosition != std::string::npos)
|
|
|
|
{
|
|
|
|
std::string key = (*it).substr(0, delimiterPosition);
|
|
|
|
delimiterPosition++;
|
|
|
|
std::string value = (*it).substr(delimiterPosition);
|
|
|
|
result[key] = value;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
LOG_WRN("Util::stringVectorToMap => record is misformed: " << (*it));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
2020-04-10 12:02:39 -05:00
|
|
|
|
wsd: faster jail setup via bind-mount
loolmount now works and supports mounting and
unmounting, plus numerous improvements,
refactoring, logging, etc.. When enabled,
binding improves the jail setup time by anywhere
from 2x to orders of magnitude (in docker, f.e.).
A new config entry mount_jail_tree controls
whether mounting is used or the old method of
linking/copying of jail contents. It is set to
true by default and falls back to linking/copying.
A test mount is done when the setting is enabled,
and if mounting fails, it's disabled to avoid noise.
Temporarily disabled for unit-tests until we can
cleanup lingering mounts after Jenkins aborts our
build job. In a future patch we will have mount/jail
cleanup as part of make.
The network/system files in /etc that need frequent
refreshing are now updated in systemplate to make
their most recent version available in the jails.
These files can change during the course of loolwsd
lifetime, and are unlikely to be updated in
systemplate after installation at all. We link to
them in the systemplate/etc directory, and if that
fails, we copy them before forking each kit
instance to have the latest.
This reworks the approach used to bind-mount the
jails and the templates such that the total is
now down to only three mounts: systemplate, lo, tmp.
As now systemplate and lotemplate are shared, they
must be mounted as readonly, this means that user/
must now be moved into tmp/user/ which is writable.
The mount-points must be recursive, because we mount
lo/ within the mount-point of systemplate (which is
the root of the jail). But because we (re)bind
recursively, and because both systemplate and
lotemplate are mounted for each jails, we need to
make them unbindable, so they wouldn't multiply the
mount-points for each jails (an explosive growth!)
Contrarywise, we don't want the mount-points to
be shared, because we don't expect to add/remove
mounts after a jail is created.
The random temp directory is now created and set
correctly, plus many logging and other improvements.
Change-Id: Iae3fda5e876cf47d2cae6669a87b5b826a8748df
Reviewed-on: https://gerrit.libreoffice.org/c/online/+/92829
Tested-by: Jenkins
Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice@gmail.com>
Reviewed-by: Ashod Nakashian <ashnakash@gmail.com>
2020-04-09 08:02:58 -05:00
|
|
|
static std::string ApplicationPath;
|
|
|
|
void setApplicationPath(const std::string& path)
|
|
|
|
{
|
|
|
|
ApplicationPath = Poco::Path(path).absolute().toString();
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string getApplicationPath()
|
|
|
|
{
|
|
|
|
return ApplicationPath;
|
|
|
|
}
|
|
|
|
|
2020-04-10 12:02:39 -05:00
|
|
|
#if !MOBILEAPP
|
|
|
|
// If OS is not mobile, it must be Linux.
|
|
|
|
std::string getLinuxVersion(){
|
|
|
|
// Read operating system info. We can read "os-release" file, located in /etc.
|
|
|
|
std::ifstream ifs("/etc/os-release");
|
|
|
|
std::string str(std::istreambuf_iterator<char>{ifs}, {});
|
|
|
|
std::vector<std::string> infoList = Util::splitStringToVector(str, '\n');
|
|
|
|
std::map<std::string, std::string> releaseInfo = Util::stringVectorToMap(infoList, '=');
|
|
|
|
|
2020-04-14 02:54:04 -05:00
|
|
|
auto it = releaseInfo.find("PRETTY_NAME");
|
|
|
|
if (it != releaseInfo.end())
|
|
|
|
{
|
|
|
|
return it->second;
|
2020-04-10 12:02:39 -05:00
|
|
|
}
|
2020-04-14 02:54:04 -05:00
|
|
|
else
|
|
|
|
{
|
2020-04-10 12:02:39 -05:00
|
|
|
return "unknown";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
2020-06-18 04:07:54 -05:00
|
|
|
StringVector tokenizeAnyOf(const std::string& s, const char* delimiters)
|
|
|
|
{
|
|
|
|
// trim from the end so that we do not have to check this exact case
|
|
|
|
// later
|
|
|
|
std::size_t length = s.length();
|
|
|
|
while (length > 0 && s[length - 1] == ' ')
|
|
|
|
--length;
|
|
|
|
|
|
|
|
if (length == 0)
|
|
|
|
return StringVector();
|
|
|
|
|
|
|
|
std::size_t delimitersLength = std::strlen(delimiters);
|
|
|
|
std::size_t start = 0;
|
|
|
|
|
|
|
|
std::vector<StringToken> tokens;
|
|
|
|
tokens.reserve(16);
|
|
|
|
|
|
|
|
while (start < length)
|
|
|
|
{
|
|
|
|
// ignore the leading whitespace
|
|
|
|
while (start < length && s[start] == ' ')
|
|
|
|
++start;
|
|
|
|
|
|
|
|
// anything left?
|
|
|
|
if (start == length)
|
|
|
|
break;
|
|
|
|
|
|
|
|
std::size_t end = s.find_first_of(delimiters, start, delimitersLength);
|
|
|
|
if (end == std::string::npos)
|
|
|
|
end = length;
|
|
|
|
|
|
|
|
// trim the trailing whitespace
|
|
|
|
std::size_t trimEnd = end;
|
|
|
|
while (start < trimEnd && s[trimEnd - 1] == ' ')
|
|
|
|
--trimEnd;
|
|
|
|
|
|
|
|
// add only non-empty tokens
|
|
|
|
if (start < trimEnd)
|
|
|
|
tokens.emplace_back(start, trimEnd - start);
|
|
|
|
|
|
|
|
start = end + 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
return StringVector(s, std::move(tokens));
|
|
|
|
}
|
2015-03-17 18:56:15 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
|