sandbox.cpp 9.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 18 19 20 21 22
#include <QtCore/QFileInfo>
#include <QtCore/QProcessEnvironment>
#include <QtCore/QSet>

#include "libssu/ssucoreconfig.h"
#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 59
Sandbox::Sandbox()
  : m_defaultConstructed(true), m_usage(UseDirectly), m_scopes(ThisProcess),
60
    m_sandboxPath(QProcessEnvironment::systemEnvironment().value("SSU_SANDBOX_DIR")),
61
    m_prepared(false){
62 63
  if (!activate()){
    qFatal("%s: Failed to activate", Q_FUNC_INFO);
64 65 66
  }
}

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

Sandbox::~Sandbox(){
74 75 76
  if (isActive()){
    deactivate();
  }
77 78 79 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
bool Sandbox::isActive() const{
86 87 88 89 90 91 92 93 94 95 96
  return s_activeInstance == this;
}

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

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

  if (m_scopes & ChildProcesses){
97
    setenv("SSU_SANDBOX_DIR", qPrintable(m_workingSandboxDir.path()), 1);
98 99 100 101
  }

  s_activeInstance = this;
  return true;
102 103
}

104 105 106 107
void Sandbox::deactivate(){
  Q_ASSERT(isActive());

  if (m_scopes & ChildProcesses){
108
    unsetenv("SSU_SANDBOX_DIR");
109 110 111 112 113
  }

  s_activeInstance = 0;
}

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

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

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

135 136 137 138 139
/**
 * Copies selected files into sandbox. Existing files in sandbox are not overwriten.
 *
 * @c QDir::NoDotAndDotDot is always added into @a filters.
 */
140
bool Sandbox::addWorldFiles(const QString &directory, QDir::Filters filters,
141
    const QStringList &filterNames, bool recurse){
142
  Q_ASSERT(!isActive());
143 144
  Q_ASSERT(!directory.isEmpty());

145 146
  if (!prepare()){
    return false;
147 148
  }

149 150 151
  const QString sandboxedDirectory = m_workingSandboxDir.filePath(
      QDir::root().relativeFilePath(
        QFileInfo(directory).absoluteFilePath()));
152

153
  if (!QFileInfo(directory).exists()){
154 155 156 157
    // 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()){
158 159
    qWarning("%s: Is not a directory: '%s'", Q_FUNC_INFO, qPrintable(directory));
    return false;
160 161
  }

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

  if (filters == QDir::NoFilter){
    filters = QDir::AllEntries;
  }

  filters |= QDir::NoDotAndDotDot;

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

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

184 185 186 187 188 189
    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;
190
        }
191 192 193 194
      } else if (!sandboxEntryInfo.isDir()){
          qWarning("%s: Failed to create sandboxed copy '%s': Is not a directory", Q_FUNC_INFO,
              qPrintable(sandboxEntryInfo.filePath()));
          return false;
195
      }
196 197 198 199 200 201

      if (recurse){
        if (!addWorldFiles(worldEntryInfo.absoluteFilePath(), filters, filterNames, true)){
          return false;
        }
      }
202 203 204 205 206 207
    } 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;
208
        }
209 210 211 212
      } else if (sandboxEntryInfo.isDir()){
          qWarning("%s: Failed to create sandboxed copy '%s': Is a directory", Q_FUNC_INFO,
              qPrintable(sandboxEntryInfo.filePath()));
          return false;
213 214 215 216
      }
    }
  }

217
  return true;
218 219
}

220 221 222 223 224
bool Sandbox::addWorldFile(const QString &file){
  return addWorldFiles(QFileInfo(file).path(), QDir::NoFilter,
      QStringList() << QFileInfo(file).fileName());
}

225 226 227 228 229 230 231 232 233 234 235 236 237
bool Sandbox::prepare(){
  Q_ASSERT(m_defaultConstructed || !m_sandboxPath.isEmpty());

  if (m_prepared){
    return true;
  }

  if (m_sandboxPath.isEmpty()){
    return true;
  }

  if (!QFileInfo(m_sandboxPath).exists()){
    qWarning("%s: Invalid sandboxPath: No such file or directory", qPrintable(m_sandboxPath));
238 239 240
    return false;
  }

241 242
  if (!QFileInfo(m_sandboxPath).isDir()){
    qWarning("%s: Invalid sandboxPath: Not a directory", qPrintable(m_sandboxPath));
243 244 245
    return false;
  }

246
  if (m_usage == UseAsSkeleton){
247
    if (m_tempDir = createTmpDir("ssu-sandbox.%1"), m_tempDir.isEmpty()){
248 249
      qWarning("%s: Failed to create sandbox directory", Q_FUNC_INFO);
      return false;
250
    }
251

252
    const QString sandboxCopyPath = QDir(m_tempDir).filePath("configroot");
253

254
    if (!copyDir(m_sandboxPath, sandboxCopyPath)){
255
      qWarning("%s: Failed to copy sandbox directory", Q_FUNC_INFO);
256
      return false;
257
    }
258

259
    m_workingSandboxDir = QDir(sandboxCopyPath);
260
  } else{
261
    m_workingSandboxDir = QDir(m_sandboxPath);
262
  }
263

264
  m_prepared = true;
265 266
  return true;
}
267

268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286
QString Sandbox::createTmpDir(const QString &nameTemplate){
  static const int REASONABLE_REPEAT_COUNT = 10;

  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());

    if (QDir().mkpath(path)){
      return path;
    }
  }

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

287 288 289 290 291 292 293 294 295 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
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;
    }
  }

  return true;
}