sandbox.cpp 10.3 KB
Newer Older
1 2 3 4 5 6 7 8 9
/**
 * @file sandbox.cpp
 * @copyright 2013 Jolla Ltd.
 * @author Martin Kampas <martin.kampas@tieto.com>
 * @date 2013
 */

#include "sandbox_p.h"

10 11
#include <stdlib.h>

12
#include <QtCore/QDir>
13
#include <QtCore/QDirIterator>
14 15 16 17
#include <QtCore/QFileInfo>
#include <QtCore/QProcessEnvironment>
#include <QtCore/QSet>

18
#include "libssu/ssucoreconfig_p.h"
19 20 21 22
#include "constants.h"

/**
 * @class Sandbox
23 24 25
 * @brief Helps to redirect file operations into sandbox directory
 *
 * The term <em>world files</em> is used to reffer files outside sandbox.
26
 *
27 28 29 30 31 32 33 34
 * To write a sandbox aware code, simply use the map() function to process paths
 * before accessing them.
 *
 * @code
 * QFile data(Sandbox::map("/usr/share/my_app/data"));
 * data.open();
 * ...
 * @endcode
35 36 37 38 39 40
 *
 * Its effect is controlled by activate() and deactivate() calls. Only one
 * Sandbox instance can be active at any time. Active sandbox is automatically
 * deactivated upon destruction.
 *
 * When constructed without arguments, path to sandbox directory is get from
41
 * @c SSU_SANDBOX_DIR environment variable.
42 43 44 45
 *
 * @attention When constructed without arguments, it is activated automatically
 * and failure to do so is reported with @c qFatal(), i.e., application will be
 * abort()ed.
46 47 48
 *
 * When constructed with @a usage UseAsSkeleton, it will first make temporary
 * copy of @a sandboxPath to work on and files in the original directory will
49
 * stay untouched.  Also see addWorldFiles().
50
 *
51
 * The argument @scopes allows to control if the sandbox will be used by this
52
 * process, its children processes (@c SSU_SANDBOX_DIR environment variable
53
 * will be exported), or both.
54 55
 */

56
Sandbox *Sandbox::s_activeInstance = 0;
57

58
Sandbox::Sandbox()
59 60 61 62 63 64 65
    : m_defaultConstructed(true), m_usage(UseDirectly), m_scopes(ThisProcess),
      m_sandboxPath(QProcessEnvironment::systemEnvironment().value("SSU_SANDBOX_DIR")),
      m_prepared(false)
{
    if (!activate()) {
        qFatal("%s: Failed to activate", Q_FUNC_INFO);
    }
66 67
}

68
Sandbox::Sandbox(const QString &sandboxPath, Usage usage, Scopes scopes)
69 70 71 72
    : m_defaultConstructed(false), m_usage(usage), m_scopes(scopes),
      m_sandboxPath(sandboxPath), m_prepared(false)
{
    Q_ASSERT(!sandboxPath.isEmpty());
73 74
}

75 76 77 78 79
Sandbox::~Sandbox()
{
    if (isActive()) {
        deactivate();
    }
80

81 82 83 84
    if (!m_tempDir.isEmpty() && QFileInfo(m_tempDir).exists()) {
        if (QProcess::execute("rm", QStringList() << "-rf" << m_tempDir) != 0) {
            qWarning("%s: Failed to remove temporary directory", Q_FUNC_INFO);
        }
85 86 87
    }
}

88 89 90
bool Sandbox::isActive() const
{
    return s_activeInstance == this;
91 92
}

93 94 95
bool Sandbox::activate()
{
    Q_ASSERT_X(s_activeInstance == 0, Q_FUNC_INFO, "Only one instance can be active!");
96

97 98 99
    if (!prepare()) {
        return false;
    }
100

101 102 103
    if (m_scopes & ChildProcesses) {
        setenv("SSU_SANDBOX_DIR", qPrintable(m_workingSandboxDir.path()), 1);
    }
104

105 106
    s_activeInstance = this;
    return true;
107 108
}

109 110 111
void Sandbox::deactivate()
{
    Q_ASSERT(isActive());
112

113 114 115
    if (m_scopes & ChildProcesses) {
        unsetenv("SSU_SANDBOX_DIR");
    }
116

117
    s_activeInstance = 0;
118 119
}

120 121
QDir Sandbox::effectiveRootDir()
{
122 123 124
    return s_activeInstance != 0 && s_activeInstance->m_scopes & ThisProcess
           ? s_activeInstance->m_workingSandboxDir
           : QDir::root();
125 126 127 128
}

QString Sandbox::map(const QString &fileName)
{
129 130 131
    return effectiveRootDir().filePath(
               QDir::root().relativeFilePath(
                   QFileInfo(fileName).absoluteFilePath()));
132 133
}

134 135
QString Sandbox::map(const QString &pathName, const QString &fileName)
{
136 137 138
    return effectiveRootDir().filePath(
               QDir::root().relativeFilePath(
                   QFileInfo(pathName + fileName).absoluteFilePath()));
139 140
}

141 142 143 144 145
/**
 * Copies selected files into sandbox. Existing files in sandbox are not overwriten.
 *
 * @c QDir::NoDotAndDotDot is always added into @a filters.
 */
146
bool Sandbox::addWorldFiles(const QString &directory, QDir::Filters filters,
147 148 149 150 151 152 153
                            const QStringList &filterNames, bool recurse)
{
    Q_ASSERT(!isActive());
    Q_ASSERT(!directory.isEmpty());

    if (!prepare()) {
        return false;
154 155
    }

156 157 158
    const QString sandboxedDirectory = m_workingSandboxDir.filePath(
                                           QDir::root().relativeFilePath(
                                               QFileInfo(directory).absoluteFilePath()));
159

160 161 162 163 164 165 166 167
    if (!QFileInfo(directory).exists()) {
        // Accept missing world directory - allow to create directories inside sandbox
        qDebug("%s: Directory does not exist - an empty one will be created instead of copied: '%s'",
               Q_FUNC_INFO, qPrintable(directory));
    } else if (!QFileInfo(directory).isDir()) {
        qWarning("%s: Is not a directory: '%s'", Q_FUNC_INFO, qPrintable(directory));
        return false;
    }
168

169 170 171 172 173 174 175 176 177 178 179
    if (!QFileInfo(sandboxedDirectory).exists()) {
        if (!QDir().mkpath(sandboxedDirectory)) {
            qWarning("%s: Failed to create sandbox directory '%s'", Q_FUNC_INFO,
                     qPrintable(sandboxedDirectory));
            return false;
        }
    } else if (!QFileInfo(sandboxedDirectory).isDir()) {
        qWarning("%s: Failed to create sandbox directory '%s': Is not a directory", Q_FUNC_INFO,
                 qPrintable(sandboxedDirectory));
        return false;
    }
180

181 182 183
    if (filters == QDir::NoFilter) {
        filters = QDir::AllEntries;
    }
184

185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
    filters |= QDir::NoDotAndDotDot;

    foreach (const QFileInfo &worldEntryInfo, QDir(directory).entryInfoList(filterNames, filters)) {

        const QFileInfo sandboxEntryInfo(QDir(sandboxedDirectory).filePath(worldEntryInfo.fileName()));

        if (worldEntryInfo.isDir()) {
            if (!sandboxEntryInfo.exists()) {
                if (!QDir(sandboxedDirectory).mkdir(worldEntryInfo.fileName())) {
                    qWarning("%s: Failed to create overlay directory '%s/%s'", Q_FUNC_INFO,
                             qPrintable(sandboxedDirectory), qPrintable(worldEntryInfo.fileName()));
                    return false;
                }
            } else if (!sandboxEntryInfo.isDir()) {
                qWarning("%s: Failed to create sandboxed copy '%s': Is not a directory", Q_FUNC_INFO,
                         qPrintable(sandboxEntryInfo.filePath()));
                return false;
            }

            if (recurse) {
                if (!addWorldFiles(worldEntryInfo.absoluteFilePath(), filters, filterNames, true)) {
                    return false;
                }
            }
        } else {
            if (!sandboxEntryInfo.exists()) {
                if (!QFile(worldEntryInfo.filePath()).copy(sandboxEntryInfo.filePath())) {
                    qWarning("%s: Failed to copy file into sandbox '%s'", Q_FUNC_INFO,
                             qPrintable(worldEntryInfo.filePath()));
                    return false;
                }
            } else if (sandboxEntryInfo.isDir()) {
                qWarning("%s: Failed to create sandboxed copy '%s': Is a directory", Q_FUNC_INFO,
                         qPrintable(sandboxEntryInfo.filePath()));
                return false;
            }
221 222 223
        }
    }

224
    return true;
225 226
}

227 228 229 230
bool Sandbox::addWorldFile(const QString &file)
{
    return addWorldFiles(QFileInfo(file).path(), QDir::NoFilter,
                         QStringList() << QFileInfo(file).fileName());
231 232
}

233 234 235
bool Sandbox::prepare()
{
    Q_ASSERT(m_defaultConstructed || !m_sandboxPath.isEmpty());
236

237 238
    if (m_prepared) {
        return true;
239
    }
240

241 242 243
    if (m_sandboxPath.isEmpty()) {
        return true;
    }
244

245 246 247
    if (!QFileInfo(m_sandboxPath).exists()) {
        qWarning("%s: Invalid sandboxPath: No such file or directory", qPrintable(m_sandboxPath));
        return false;
248
    }
249

250 251 252 253
    if (!QFileInfo(m_sandboxPath).isDir()) {
        qWarning("%s: Invalid sandboxPath: Not a directory", qPrintable(m_sandboxPath));
        return false;
    }
254

255 256 257 258 259
    if (m_usage == UseAsSkeleton) {
        if (m_tempDir = createTmpDir("ssu-sandbox.%1"), m_tempDir.isEmpty()) {
            qWarning("%s: Failed to create sandbox directory", Q_FUNC_INFO);
            return false;
        }
260

261
        const QString sandboxCopyPath = QDir(m_tempDir).filePath("configroot");
262

263 264 265 266
        if (!copyDir(m_sandboxPath, sandboxCopyPath)) {
            qWarning("%s: Failed to copy sandbox directory", Q_FUNC_INFO);
            return false;
        }
267

268 269 270
        m_workingSandboxDir = QDir(sandboxCopyPath);
    } else {
        m_workingSandboxDir = QDir(m_sandboxPath);
271 272
    }

273 274
    m_prepared = true;
    return true;
275 276
}

277 278 279
QString Sandbox::createTmpDir(const QString &nameTemplate)
{
    static const int REASONABLE_REPEAT_COUNT = 10;
280

281 282 283 284 285 286
    for (int i = 0; i < REASONABLE_REPEAT_COUNT; ++i) {
        QString path;
        int suffix = 0;
        do {
            path = QDir::temp().filePath(nameTemplate.arg(++suffix));
        } while (QFileInfo(path).exists());
287

288 289 290 291
        if (QDir().mkpath(path)) {
            return path;
        }
    }
292

293 294 295
    qWarning("%s: Failed to create temporary directory", Q_FUNC_INFO);
    return QString();
}
296

297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328
bool Sandbox::copyDir(const QString &directory, const QString &newName)
{
    const QDir sourceRoot(directory);
    const QDir destinationRoot(newName);

    QDirIterator it(directory, QDir::AllEntries | QDir::NoDotAndDotDot, QDirIterator::Subdirectories);
    while (it.hasNext()) {
        it.next();

        const QFileInfo destination = QFileInfo(destinationRoot.filePath(
                                                    sourceRoot.relativeFilePath(it.filePath())));

        if (it.fileInfo().isDir()) {
            if (!QDir().mkpath(destination.absoluteFilePath())) {
                qWarning("%s: Failed to mkpath '%s'", Q_FUNC_INFO, qPrintable(destination.absolutePath()));
                return false;
            }
        } else if (it.fileInfo().isFile()) {
            if (!QDir().mkpath(destination.absolutePath())) {
                qWarning("%s: Failed to mkpath '%s'", Q_FUNC_INFO, qPrintable(destination.absolutePath()));
                return false;
            }

            if (!QFile::copy(it.fileInfo().absoluteFilePath(), destination.absoluteFilePath())) {
                qWarning("%s: Failed to copy file '%s'", Q_FUNC_INFO, qPrintable(it.filePath()));
                return false;
            }
        } else {
            qWarning("%s: Cannot copy other than regular files: '%s'", Q_FUNC_INFO,
                     qPrintable(it.filePath()));
            return false;
        }
329 330
    }

331
    return true;
332
}