base for the Atlas JavaScript engine : implemented the Engine, a virtual filesytem and prepared the library system.

This commit is contained in:
faraphel 2024-08-12 13:55:33 +02:00
parent 719d815b2d
commit 308124c99f
28 changed files with 655 additions and 197 deletions

199
.gitignore vendored
View file

@ -1,196 +1,7 @@
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# IDE
.idea/
.venv/
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# ---> C++
# Prerequisites
*.d
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
*.smod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app
# CMake
cmake-*/

6
.gitmodules vendored
View file

@ -1,9 +1,9 @@
[submodule ".mods/Atlas"]
path = .mods/Atlas
url = https://git.faraphel.fr/Atlas/Atlas-Mod
[submodule "library-szs"]
[submodule "external/szs"]
path = external/szs
url = https://git.faraphel.fr/Atlas/library-szs
[submodule "library-wit"]
path = external/szs
[submodule "external/wit"]
path = external/wit
url = https://git.faraphel.fr/Atlas/library-wit

1
.mods/Atlas Submodule

@ -0,0 +1 @@
Subproject commit 997d361d05a2f022d027118f6d1cb5c9642586a5

50
CMakeLists.txt Normal file
View file

@ -0,0 +1,50 @@
cmake_minimum_required(VERSION 3.28)
project(Atlas-Launcher LANGUAGES CXX)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt6 REQUIRED COMPONENTS
Core
Gui
Widgets
Qml
)
add_executable(Atlas-Launcher
source/main.cpp
source/javascript/module/AtlasJsModule.cpp
source/javascript/module/AtlasJsModule.hpp
source/javascript/engine/AtlasJsEngine.cpp
source/javascript/engine/AtlasJsEngine.hpp
source/javascript/module/debug/DebugJsModule.cpp
source/javascript/module/debug/DebugJsModule.hpp
source/javascript/module/image/ImageJsModule.cpp
source/javascript/module/image/ImageJsModule.hpp
source/javascript/module/image/ImageJsObject.cpp
source/javascript/module/image/ImageJsObject.hpp
source/javascript/module/_base/BaseJsModule.cpp
source/javascript/module/_base/BaseJsModule.hpp
source/javascript/module/file/FileJsModule.cpp
source/javascript/module/file/FileJsModule.hpp
)
target_link_libraries(Atlas-Launcher PRIVATE
# Tools
RVFS
SZS
# Qt Framework
Qt::Core
Qt::Gui
Qt::Widgets
Qt::Qml
)
add_subdirectory(external/rvfs)
add_subdirectory(external/szs)

27
external/rvfs/CMakeLists.txt vendored Normal file
View file

@ -0,0 +1,27 @@
cmake_minimum_required(VERSION 3.28)
project(RVFS LANGUAGES CXX)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt6 REQUIRED COMPONENTS
Core
)
add_library(RVFS STATIC
source/RestrictedVirtualFileSystem.cpp
)
target_include_directories(RVFS PUBLIC
include/
)
target_link_libraries(RVFS PRIVATE
# Qt Framework
Qt::Core
)
add_subdirectory(tests)

10
external/rvfs/README.md vendored Normal file
View file

@ -0,0 +1,10 @@
# Restricted Virtual File System
This is a micro-library used to create a virtual file system to restrict access
to the host file system while still being able to mount directories that can be accessed.
As of right now, this is an incredibly simple implementation of this system that
work by mapping virtual directory path to a corresponding real directory path, replacing
them while resolving the path and reject all others paths that have not been mounted
to avoid access to other files.
It does not emulate a real file system nor allow control over permissions of the files.

View file

@ -0,0 +1,62 @@
#pragma once
#include <QDir>
#include <QMap>
namespace rvfs {
/**
* Represent a virtual restricted file system.
* Real directory can be mounted to safely interact with the real file system.
*/
class RestrictedVirtualFileSystem {
public:
explicit RestrictedVirtualFileSystem();
/**
* Get all the mounted directories with their path in the virtual file system and in the real file system
* @return all the mounted directories with their path in the virtual file system and in the real file system
*/
QMap<QString, QString> getMountedDirectories();
/**
* Mount a real directory into the virtual file system
* @param destination the path to the directory in the virtual file system
* @param source the path to the directory in the real file system
*/
void mountDirectory(const QDir& destination, const QDir& source);
/**
* Unmount a directory from the virtual file system
* @param destination the path to the directory in the virtual file system
*/
void unmountDirectory(const QDir& destination);
/**
* Get the current working directory in the virtual file system
* @return the current working directory in the virtual file system
*/
QDir getWorkingDirectory();
/**
* Change the current working directory in the virtual file system
* @param path path to the new virtual working directory
*/
void setWorkingDirectory(const QDir& path);
/**
* Resolve a path inside the virtual file system into a path in the real file system
* @param path the path in the virtual file system
* @return the matching path in the real file system
*/
QString resolvePath(QString path);
private:
/// the current working directory
QDir workingDirectory;
/// store the path in the virtual file system of a directory in the real file system
QMap<QString, QString> mounts;
};
}

View file

@ -0,0 +1,65 @@
#include "RestrictedVirtualFileSystem.hpp"
namespace rvfs {
RestrictedVirtualFileSystem::RestrictedVirtualFileSystem() = default;
QMap<QString, QString> RestrictedVirtualFileSystem::getMountedDirectories() {
return this->mounts;
}
void RestrictedVirtualFileSystem::mountDirectory(const QDir& destination, const QDir& source) {
// check if the source directory does exists
if (!source.exists())
// TODO(Faraphel): should be a custom error ?
throw std::runtime_error("The source directory does not exists.");
// mount it by associating our virtual folder with our real one
this->mounts[destination.absolutePath()] = source.absolutePath();
}
void RestrictedVirtualFileSystem::unmountDirectory(const QDir& destination) {
// remove the mounted directory from the map
this->mounts.remove(destination.absolutePath());
}
QDir RestrictedVirtualFileSystem::getWorkingDirectory() {
return this->workingDirectory;
}
void RestrictedVirtualFileSystem::setWorkingDirectory(const QDir& path) {
this->workingDirectory = path;
}
QString RestrictedVirtualFileSystem::resolvePath(QString path) {
if (QFileInfo(path).isRelative()) {
// if the path is relative, make it absolute with the current working directory
path = this->workingDirectory.absoluteFilePath(path);
} else {
// if the path is absolute, make sure to resolve the special syntax like ".."
path = QFileInfo(path).absoluteFilePath();
}
bool resolved = false;
// go through all the mounted directories
for (auto const& [destination, source] : this->mounts.asKeyValueRange()) {
// if the path is inside a mounted directory
if (path.startsWith(destination)) {
// replace the mounted directory by the original real one
path = path.replace(0, destination.length(), source);
// mark the path as resolved and break
resolved = true;
break;
}
}
// if the path has not been resolved, it is in an invalid or forbidden location
if (!resolved)
// TODO(Faraphel): should be a custom error ?
throw std::runtime_error("cannot resolve path: invalid.");
return path;
}
}

36
external/rvfs/tests/CMakeLists.txt vendored Normal file
View file

@ -0,0 +1,36 @@
cmake_minimum_required(VERSION 3.28)
project(Test-RVFS LANGUAGES CXX)
enable_testing()
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Packages
find_package(Qt6 REQUIRED COMPONENTS
Core
)
# Test executable
add_executable(Test-RVFS
test-restriction.cpp
)
# Libraries
target_link_libraries(Test-RVFS PRIVATE
# Code to test
RVFS
# Qt Framework
Qt::Core
)
# Copy the assets to run the tests
file(COPY _assets DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
# Test
add_test(NAME Test-RVFS COMMAND Test-RVFS)

View file

@ -0,0 +1 @@
This is a simple text file.

View file

@ -0,0 +1,37 @@
#include <iostream>
#include <RestrictedVirtualFileSystem.hpp>
int main() {
// TODO(Faraphel): use a real unit test framework ?
// create a basic rvfs
rvfs::RestrictedVirtualFileSystem fs;
// mount a directory into our system
fs.mountDirectory(QDir("/assets/"), QDir("./_assets/"));
// TEST: check if a file outside of an allowed mounted directory can't be resolved
try {
const QString& path = fs.resolvePath("/content.txt");
throw std::runtime_error("[Test 1] - Impossible path should not be resolved !");
} catch (const std::runtime_error& exception) {}
// TEST: check if a file in a mounted directory can be resolved
try {
const QString& path = fs.resolvePath("/assets/content.txt");
auto file = QFile(path);
file.open(QIODeviceBase::Text | QIODevice::ReadOnly);
std::cout << file.readAll().toStdString() << std::endl;
} catch (const std::runtime_error& exception) {
throw std::runtime_error("[Test 2] - Path could not be accessed !");
}
// TEST: cannot bypass security with the ".." parent directory syntax
try {
const QString& path = fs.resolvePath("/assets/../content.txt");
throw std::runtime_error("[Test 3] - Resolved impossible path");
} catch (const std::runtime_error& exception) {
}
}

1
external/szs vendored Submodule

@ -0,0 +1 @@
Subproject commit 3b292fbcad336ebb2d9137a379c8a9cdaf945162

1
external/wit vendored Submodule

@ -0,0 +1 @@
Subproject commit 80328498a1f2446a67a1df55900edb63e6e93bb0

View file

@ -0,0 +1,20 @@
#include "AtlasJsEngine.hpp"
#include "../module/AtlasJsModule.hpp"
namespace atlas::js {
AtlasJsEngine::AtlasJsEngine(QObject *parent) : QJSEngine(parent) {
// instanciate a new Atlas module
this->moduleAtlas = std::make_unique<AtlasJsModule>(this);
// convert it into a javascript object
const QJSValue jsModuleAtlas = this->newQObject(this->moduleAtlas.get());
// make it accessible as "atlas" in the javascript code
this->globalObject().setProperty("atlas", jsModuleAtlas);
}
}

View file

@ -0,0 +1,24 @@
#pragma once
#include <QJSEngine>
#include <QList>
#include <QDir>
#include "../module/AtlasJsModule.hpp"
namespace atlas::js {
/**
* This class represent a Qt JavaScript engine modified to support the Atlas framework.
*/
class AtlasJsEngine : public QJSEngine {
public:
explicit AtlasJsEngine(QObject* parent);
private:
std::shared_ptr<AtlasJsModule> moduleAtlas;
};
}

View file

@ -0,0 +1,47 @@
#include "AtlasJsModule.hpp"
#include <QJSEngine>
#include "debug/DebugJsModule.hpp"
#include "image/ImageJsModule.hpp"
AtlasJsModule::AtlasJsModule(QJSEngine* engine, QObject* parent) : QObject(parent) {
// set the engine
this->engine = engine;
// load the submodules
this->submodules[QStringLiteral("debug")] = std::make_shared<DebugJsModule>(this->engine);
this->submodules[QStringLiteral("image")] = std::make_shared<ImageJsModule>(this->engine);
}
QJSValue AtlasJsModule::require(const QString& name) {
std::shared_ptr<BaseJsModule> submodule;
try {
// get the submodule from its name
submodule = this->submodules.at(name);
} catch (const std::out_of_range&) {
// if not found, throw a Javascript error and return null
this->engine->throwError(
QJSValue::ReferenceError,
QStringLiteral("the module \"") + name + QStringLiteral("\" does not exist.")
);
return QJSValue(QJSPrimitiveNull());
}
// return the module as a new javascript object
return this->engine->newQObject(submodule.get());
}
QJSValueList AtlasJsModule::getVersion() {
// TODO(Faraphel): should be stored somewhere else.
// TODO(Faraphel): The type should implement an easy comparison system, if possible
QJSValueList version;
version.append(1);
version.append(0);
version.append(0);
return version;
}

View file

@ -0,0 +1,34 @@
#pragma once
#include <QObject>
#include <QJSValue>
#include "_base/BaseJsModule.hpp"
class AtlasJsModule : public QObject {
Q_OBJECT
public:
explicit AtlasJsModule(QJSEngine* engine, QObject* parent = nullptr);
/**
* Import an Atlas submodule
* @param name the name of the module to import
* @return the module
*/
Q_INVOKABLE QJSValue require(const QString& name);
/**
* Get the version of the Atlas Javascript engine
* @return the Atlas version
*/
Q_INVOKABLE static QJSValueList getVersion();
private:
/// the parent JavaScript engine
QJSEngine* engine;
/// the submodules contained in this module
std::map<QString, std::shared_ptr<BaseJsModule>> submodules;
};

View file

@ -0,0 +1,6 @@
#include "BaseJsModule.hpp"
BaseJsModule::BaseJsModule(QJSEngine *engine) {
this->engine = engine;
}

View file

@ -0,0 +1,19 @@
#pragma once
#include <QObject>
#include <QJSEngine>
/**
* Base class for an Atlas JavaScript submodule
*/
class BaseJsModule : public QObject {
Q_OBJECT
public:
explicit BaseJsModule(QJSEngine* engine);
protected:
/// The javascript engine running the module
QJSEngine* engine;
};

View file

@ -0,0 +1,10 @@
#include "DebugJsModule.hpp"
#include <QDebug>
DebugJsModule::DebugJsModule(QJSEngine* engine) : BaseJsModule(engine) {}
void DebugJsModule::information(const QString& text) {
qDebug() << text;
}

View file

@ -0,0 +1,19 @@
#pragma once
#include "../_base/BaseJsModule.hpp"
/**
* This module implements useful feature to help with debugging
*/
class DebugJsModule : public BaseJsModule {
Q_OBJECT
public:
explicit DebugJsModule(QJSEngine* engine);
/**
* Print an informational message
*/
Q_INVOKABLE static void information(const QString& text);
};

View file

@ -0,0 +1,12 @@
#include "FileJsModule.hpp"
#include <QFile>
FileJsModule::FileJsModule(QJSEngine *engine, const QList<QDir>& allowedDirectories) : BaseJsModule(engine) {
this->allowedDirectories = allowedDirectories;
}
QJSValue FileJsModule::open(const QString& path) {
QFile file(path);
}

View file

@ -0,0 +1,18 @@
#pragma once
#include <QDir>
#include "../_base/BaseJsModule.hpp"
class FileJsModule : public BaseJsModule {
Q_OBJECT
public:
explicit FileJsModule(QJSEngine* engine, const QList<QDir>& allowedDirectories);
Q_INVOKABLE QJSValue open(const QString& path);
private:
QList<QDir> allowedDirectories;
};

View file

@ -0,0 +1,11 @@
#include "ImageJsModule.hpp"
#include "ImageJsObject.hpp"
ImageJsModule::ImageJsModule(QJSEngine* engine) : BaseJsModule(engine) {}
QJSValue ImageJsModule::load(const QString& path) {
// TODO(Faraphel): can the new lead to a memory leak ?
return this->engine->newQObject(new ImageJsObject(path));
}

View file

@ -0,0 +1,23 @@
#pragma once
#include <QJSValue>
#include "ImageJsObject.hpp"
#include "../_base/BaseJsModule.hpp"
/**
* A module to manipulate image file
*/
class ImageJsModule : public BaseJsModule {
Q_OBJECT
public:
explicit ImageJsModule(QJSEngine* engine);
/**
* Load an image file
* @param
*/
Q_INVOKABLE QJSValue load(const QString& path);
};

View file

@ -0,0 +1,20 @@
#include "ImageJsObject.hpp"
#include <QImage>
ImageJsObject::ImageJsObject(const QString& path) {
this->_internal = std::make_unique<QImage>(path);
}
std::size_t ImageJsObject::getWidth() {
return this->_internal->width();
}
std::size_t ImageJsObject::getHeight() {
return this->_internal->height();
}
QSize ImageJsObject::getSize() {
return this->_internal->size();
}

View file

@ -0,0 +1,22 @@
#pragma once
#include <QObject>
#include <QImage>
class ImageJsObject : public QObject {
Q_OBJECT
public:
explicit ImageJsObject(const QString& path);
Q_INVOKABLE std::size_t getWidth();
Q_INVOKABLE std::size_t getHeight();
Q_INVOKABLE QSize getSize();
// TODO(Faraphel): implement copy / paste, fill, shapes, mirror, resize, expand, shrink, crop, map...
private:
std::unique_ptr<QImage> _internal;
};

71
source/main.cpp Normal file
View file

@ -0,0 +1,71 @@
#include <iostream>
#include "javascript/engine/AtlasJsEngine.hpp"
#include <QApplication>
#include <QMainWindow>
#include <QTextEdit>
#include <QPushButton>
#include <QLayout>
#include <QVBoxLayout>
#include <QDir>
#include <QString>
#include <QTemporaryDir>
#include <RestrictedVirtualFileSystem.hpp>
int main(int argc, char* argv[]) {
// application informations
QApplication application(argc, argv);
QApplication::setOrganizationDomain("faraphel.fr");
QApplication::setApplicationName("Atlas-Launcher");
QApplication::setApplicationVersion("1.0.0");
// create a restricted virtual file system
const auto fileSystem = std::make_shared<rvfs::RestrictedVirtualFileSystem>();
// mount the cache directory
fileSystem->mountDirectory(
QDir("/cache/"),
QDir("/home/faraphel/Documents/Projects/Atlas-Launcher/source/")
);
// mount the temporary directory
const auto temporaryDir = QTemporaryDir();
fileSystem->mountDirectory(
QDir("/tmp/"),
QDir(temporaryDir.path())
);
// create a small window
auto mainWindow = std::make_shared<QMainWindow>();
auto widget = std::make_shared<QWidget>();
mainWindow->setCentralWidget(widget.get());
auto layout = std::make_shared<QVBoxLayout>();
widget->setLayout(layout.get());
auto input = std::make_shared<QTextEdit>();
layout->addWidget(input.get());
auto submit = std::make_shared<QPushButton>();
layout->addWidget(submit.get());
submit->setText("Run");
auto output = std::make_shared<QTextEdit>();
layout->addWidget(output.get());
output->setEnabled(false);
QObject::connect(submit.get(), &QPushButton::clicked, [&]() {
auto engine = std::make_shared<atlas::js::AtlasJsEngine>(nullptr);
QJSValue value = engine->evaluate(input->toPlainText());
output->setText(value.toString());
});
mainWindow->show();
return QApplication::exec();
}