extract shared mosis-core library from sandbox APIs

Create core/ directory with platform-agnostic sandbox components:
- Timer manager, JSON API, Crypto API, Virtual FS
- Lua sandbox, Permission gate, Audit log, Rate limiter
- Platform abstraction interfaces (IAssetInterface, IFilesystemInterface)
- Platform-agnostic logger with Android/Desktop implementations

Update designer to link against mosis-core library instead of
including sandbox sources directly.

This is the foundation for unifying the Android service and
desktop designer to share the same codebase.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-19 11:57:10 +01:00
parent f41eda6f62
commit 33841516f1
34 changed files with 7134 additions and 13 deletions

100
core/CMakeLists.txt Normal file
View File

@@ -0,0 +1,100 @@
cmake_minimum_required(VERSION 3.18)
project(mosis-core VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Core library sources - portable sandbox APIs
set(MOSIS_CORE_SOURCES
# Utility
src/logger.cpp
# Sandbox APIs (portable)
src/sandbox/timer_manager.cpp
src/sandbox/json_api.cpp
src/sandbox/crypto_api.cpp
src/sandbox/virtual_fs.cpp
src/sandbox/lua_sandbox.cpp
src/sandbox/permission_gate.cpp
src/sandbox/audit_log.cpp
src/sandbox/rate_limiter.cpp
src/sandbox/path_sandbox.cpp
)
# Optional sources that require additional dependencies
if(MOSIS_ENABLE_DATABASE)
list(APPEND MOSIS_CORE_SOURCES src/sandbox/database_manager.cpp)
endif()
if(MOSIS_ENABLE_NETWORK)
list(APPEND MOSIS_CORE_SOURCES
src/sandbox/network_manager.cpp
src/sandbox/http_validator.cpp
)
endif()
# Create static library
add_library(mosis-core STATIC ${MOSIS_CORE_SOURCES})
# Include directories
target_include_directories(mosis-core PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include/mosis>
$<INSTALL_INTERFACE:include>
)
# Also add internal include path for relative includes within library
target_include_directories(mosis-core PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include/mosis/sandbox
${CMAKE_CURRENT_SOURCE_DIR}/include/mosis/util
${CMAKE_CURRENT_SOURCE_DIR}/include/mosis/apps
)
# Platform-specific definitions
if(ANDROID)
target_compile_definitions(mosis-core PUBLIC MOSIS_PLATFORM_ANDROID)
elseif(WIN32)
target_compile_definitions(mosis-core PUBLIC MOSIS_PLATFORM_WINDOWS)
# Windows crypto library
target_link_libraries(mosis-core PRIVATE bcrypt)
elseif(APPLE)
target_compile_definitions(mosis-core PUBLIC MOSIS_PLATFORM_MACOS)
else()
target_compile_definitions(mosis-core PUBLIC MOSIS_PLATFORM_LINUX)
endif()
# Lua is required - parent project must provide it
if(TARGET lua_static)
target_link_libraries(mosis-core PUBLIC lua_static)
elseif(TARGET lua)
target_link_libraries(mosis-core PUBLIC lua)
else()
# Try to find Lua
find_package(Lua QUIET)
if(LUA_FOUND)
target_include_directories(mosis-core PUBLIC ${LUA_INCLUDE_DIR})
target_link_libraries(mosis-core PUBLIC ${LUA_LIBRARIES})
else()
message(WARNING "Lua not found - parent project must provide Lua target")
endif()
endif()
# JSON library (nlohmann/json - header only)
if(TARGET nlohmann_json::nlohmann_json)
target_link_libraries(mosis-core PUBLIC nlohmann_json::nlohmann_json)
else()
find_package(nlohmann_json QUIET)
if(nlohmann_json_FOUND)
target_link_libraries(mosis-core PUBLIC nlohmann_json::nlohmann_json)
endif()
endif()
# SQLite for database_manager (optional)
if(MOSIS_ENABLE_DATABASE)
find_package(SQLite3 REQUIRED)
target_link_libraries(mosis-core PRIVATE SQLite::SQLite3)
target_compile_definitions(mosis-core PRIVATE MOSIS_HAS_SQLITE)
endif()
# Export compile commands for IDE support
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

View File

@@ -0,0 +1,23 @@
// app_api.h - Lua API bindings for app management
// Milestone 10: Device-Side App Management
#pragma once
#include <string>
struct lua_State;
namespace mosis {
class AppManager;
class UpdateService;
// Register Lua APIs for app management
// - mosis.apps.* - System apps only (App Store, Settings)
// - mosis.app.* - All apps (info about current app)
void RegisterAppAPIs(lua_State* L,
AppManager* app_manager,
UpdateService* update_service,
const std::string& current_app_id,
bool is_system_app);
} // namespace mosis

View File

@@ -0,0 +1,167 @@
// app_manager.h - App installation and management
// Milestone 10: Device-Side App Management
#pragma once
#include <string>
#include <vector>
#include <map>
#include <optional>
#include <functional>
#include <mutex>
#include <chrono>
#include <cstdint>
namespace mosis {
// Forward declarations
class LuaSandboxManager;
// Information about an installed app
struct InstalledApp {
std::string package_id;
std::string name;
std::string version_name;
int version_code = 0;
std::string install_path;
std::vector<std::string> permissions;
std::chrono::system_clock::time_point installed_at;
std::chrono::system_clock::time_point updated_at;
int64_t package_size = 0;
int64_t data_size = 0;
bool is_system_app = false;
std::string entry_point;
std::string icon_path;
std::string developer_name;
};
// Progress stages during installation
struct InstallProgress {
enum class Stage {
Downloading,
Verifying,
Extracting,
Registering,
Complete,
Failed
};
Stage stage = Stage::Downloading;
float progress = 0.0f; // 0.0 - 1.0
std::string error;
static const char* StageName(Stage s) {
switch (s) {
case Stage::Downloading: return "downloading";
case Stage::Verifying: return "verifying";
case Stage::Extracting: return "extracting";
case Stage::Registering: return "registering";
case Stage::Complete: return "complete";
case Stage::Failed: return "failed";
default: return "unknown";
}
}
};
using ProgressCallback = std::function<void(const InstallProgress&)>;
// Manifest parsed from package
struct AppManifest {
std::string id;
std::string name;
std::string version;
int version_code = 0;
std::string entry;
std::string icon;
std::string description;
std::string developer_name;
std::string developer_email;
std::vector<std::string> permissions;
int min_api_version = 1;
};
class AppManager {
public:
explicit AppManager(const std::string& data_root);
~AppManager();
// Prevent copying
AppManager(const AppManager&) = delete;
AppManager& operator=(const AppManager&) = delete;
// Installation from URL
bool Install(const std::string& package_url,
const std::string& signature,
ProgressCallback callback);
// Installation from local file
bool InstallFromFile(const std::string& package_path,
ProgressCallback callback);
// Uninstallation
bool Uninstall(const std::string& package_id, bool keep_data = false);
// Updates
bool Update(const std::string& package_id,
const std::string& package_url,
const std::string& signature,
ProgressCallback callback);
// Query installed apps
std::vector<InstalledApp> GetInstalledApps() const;
std::optional<InstalledApp> GetApp(const std::string& package_id) const;
bool IsInstalled(const std::string& package_id) const;
// Data management
int64_t GetAppDataSize(const std::string& package_id) const;
bool ClearAppData(const std::string& package_id);
bool ClearAppCache(const std::string& package_id);
bool BackupAppData(const std::string& package_id);
bool RestoreAppData(const std::string& package_id);
// App launching
bool LaunchApp(const std::string& package_id);
bool StopApp(const std::string& package_id);
bool IsAppRunning(const std::string& package_id) const;
// Integration with sandbox manager
void SetSandboxManager(LuaSandboxManager* manager);
// Get paths
std::string GetDataRoot() const { return m_data_root; }
std::string GetAppPath(const std::string& package_id) const;
std::string GetAppDataPath(const std::string& package_id) const;
std::string GetAppCachePath(const std::string& package_id) const;
// System apps registration
void RegisterSystemApp(const InstalledApp& app);
private:
// Package verification
bool VerifyPackage(const std::string& path);
bool VerifySignature(const std::string& path, const std::string& signature);
// Package operations
std::optional<AppManifest> ExtractManifest(const std::string& package_path);
bool ExtractPackage(const std::string& package_path, const std::string& dest_path);
// Download helper
bool DownloadFile(const std::string& url, const std::string& dest_path,
std::function<void(float)> progress_callback);
// Registry persistence
void LoadInstalledApps();
void SaveInstalledApps();
// Directory size calculation
int64_t CalculateDirectorySize(const std::string& path) const;
// Generate unique ID
std::string GenerateUUID() const;
std::string m_data_root;
LuaSandboxManager* m_sandbox_manager = nullptr;
mutable std::mutex m_mutex;
std::map<std::string, InstalledApp> m_installed_apps;
};
} // namespace mosis

View File

@@ -0,0 +1,57 @@
#pragma once
#include <string>
#include <vector>
#include <memory>
namespace mosis {
/**
* Platform-agnostic interface for loading assets.
* Android implements this using AAssetManager.
* Desktop implements this using filesystem operations.
*/
class IAssetInterface {
public:
virtual ~IAssetInterface() = default;
/**
* Read entire file contents as bytes.
* @param path Relative path to asset (e.g., "apps/home/home.rml")
* @return File contents, or empty vector if not found
*/
virtual std::vector<uint8_t> ReadFile(const std::string& path) = 0;
/**
* Read entire file contents as string.
* @param path Relative path to asset
* @return File contents, or empty string if not found
*/
virtual std::string ReadFileString(const std::string& path) = 0;
/**
* Check if an asset exists.
* @param path Relative path to asset
* @return true if asset exists
*/
virtual bool Exists(const std::string& path) = 0;
/**
* List files in a directory.
* @param path Relative path to directory
* @return List of filenames (not full paths)
*/
virtual std::vector<std::string> ListDirectory(const std::string& path) = 0;
/**
* Get the absolute path for an asset (if applicable).
* On Android this may return empty as assets are in APK.
* @param path Relative path to asset
* @return Absolute path or empty string
*/
virtual std::string GetAbsolutePath(const std::string& path) = 0;
};
using AssetInterfacePtr = std::shared_ptr<IAssetInterface>;
} // namespace mosis

View File

@@ -0,0 +1,98 @@
#pragma once
#include <string>
#include <vector>
#include <memory>
#include <functional>
namespace mosis {
/**
* Platform-agnostic interface for filesystem operations.
* Used for app data storage, not assets.
*/
class IFilesystemInterface {
public:
virtual ~IFilesystemInterface() = default;
/**
* Get the base data directory for apps.
* Android: /data/data/com.omixlab.mosis/files/
* Desktop: ./data/ or configurable
*/
virtual std::string GetDataRoot() = 0;
/**
* Get the apps installation directory.
* Contains installed app packages.
*/
virtual std::string GetAppsDirectory() = 0;
/**
* Get app-specific data directory.
* @param app_id Application ID (e.g., "com.example.app")
*/
virtual std::string GetAppDataDirectory(const std::string& app_id) = 0;
/**
* Get app-specific cache directory.
*/
virtual std::string GetAppCacheDirectory(const std::string& app_id) = 0;
/**
* Create directory if it doesn't exist.
* @return true on success
*/
virtual bool CreateDirectory(const std::string& path) = 0;
/**
* Check if path exists.
*/
virtual bool Exists(const std::string& path) = 0;
/**
* Check if path is a directory.
*/
virtual bool IsDirectory(const std::string& path) = 0;
/**
* Remove file or directory.
* @param recursive If true, remove directory contents
*/
virtual bool Remove(const std::string& path, bool recursive = false) = 0;
/**
* Read file contents.
*/
virtual std::vector<uint8_t> ReadFile(const std::string& path) = 0;
/**
* Write file contents.
*/
virtual bool WriteFile(const std::string& path, const std::vector<uint8_t>& data) = 0;
/**
* List directory contents.
*/
virtual std::vector<std::string> ListDirectory(const std::string& path) = 0;
/**
* Get file size.
* @return Size in bytes, or -1 if not found
*/
virtual int64_t GetFileSize(const std::string& path) = 0;
/**
* Copy file.
*/
virtual bool CopyFile(const std::string& src, const std::string& dst) = 0;
/**
* Move/rename file.
*/
virtual bool MoveFile(const std::string& src, const std::string& dst) = 0;
};
using FilesystemInterfacePtr = std::shared_ptr<IFilesystemInterface>;
} // namespace mosis

View File

@@ -0,0 +1,94 @@
#pragma once
#include <string>
#include <vector>
#include <mutex>
#include <chrono>
namespace mosis {
enum class AuditEvent {
// Lifecycle
AppStart,
AppStop,
// Permissions
PermissionCheck,
PermissionGranted,
PermissionDenied,
// Network
NetworkRequest,
NetworkBlocked,
// Storage
FileAccess,
FileBlocked,
DatabaseAccess,
// Hardware
CameraAccess,
MicrophoneAccess,
LocationAccess,
// Security
SandboxViolation,
ResourceLimitHit,
RateLimitHit,
// Other
Custom
};
struct AuditEntry {
std::chrono::system_clock::time_point timestamp;
AuditEvent event;
std::string app_id;
std::string details;
bool success;
};
class AuditLog {
public:
explicit AuditLog(size_t max_entries = 10000);
// Log an event
void Log(AuditEvent event, const std::string& app_id,
const std::string& details = "", bool success = true);
// Query entries (returns most recent first)
std::vector<AuditEntry> GetEntries(size_t count = 100) const;
std::vector<AuditEntry> GetEntriesForApp(const std::string& app_id,
size_t count = 100) const;
std::vector<AuditEntry> GetEntriesByEvent(AuditEvent event,
size_t count = 100) const;
// Statistics
size_t GetTotalEntries() const;
size_t GetStoredEntries() const;
size_t CountEvents(AuditEvent event, const std::string& app_id = "") const;
// Clear all entries
void Clear();
// Convert event to string for logging
static const char* EventToString(AuditEvent event);
private:
mutable std::mutex m_mutex;
std::vector<AuditEntry> m_entries;
size_t m_max_entries;
size_t m_write_index = 0;
size_t m_total_logged = 0;
bool m_wrapped = false;
};
// Global audit log (singleton)
AuditLog& GetAuditLog();
} // namespace mosis
// Convenience alias
using AuditLog = mosis::AuditLog;
using AuditEvent = mosis::AuditEvent;
using AuditEntry = mosis::AuditEntry;

View File

@@ -0,0 +1,52 @@
#pragma once
#include <string>
#include <cstdint>
#include <random>
#include <mutex>
struct lua_State;
namespace mosis {
// Per-app cryptographically secure RNG
class SecureRandom {
public:
SecureRandom();
// Get random bytes as binary string
std::string GetBytes(size_t count);
// Get random integer in range [min, max]
int64_t GetInt(int64_t min, int64_t max);
// Get random double in range [0.0, 1.0)
double GetDouble();
private:
std::random_device m_rd;
std::mt19937_64 m_gen;
std::mutex m_mutex;
};
// Hash algorithms supported
enum class HashAlgorithm {
SHA256,
SHA512,
SHA1,
MD5
};
// Compute hash of data
std::string ComputeHash(HashAlgorithm algo, const std::string& data);
// Compute HMAC of data with key
std::string ComputeHMAC(HashAlgorithm algo, const std::string& key, const std::string& data);
// Register crypto.* APIs as globals
void RegisterCryptoAPI(lua_State* L);
// Register secure math.random replacement (removes math.randomseed)
void RegisterSecureMathRandom(lua_State* L, SecureRandom* rng);
} // namespace mosis

View File

@@ -0,0 +1,88 @@
#pragma once
#include <string>
#include <vector>
#include <variant>
#include <optional>
#include <memory>
#include <unordered_map>
struct sqlite3;
struct lua_State;
namespace mosis {
// SQL value types
using SqlValue = std::variant<std::nullptr_t, int64_t, double, std::string, std::vector<uint8_t>>;
using SqlRow = std::vector<SqlValue>;
using SqlResult = std::vector<SqlRow>;
struct DatabaseLimits {
size_t max_database_size = 50 * 1024 * 1024; // 50 MB per database
int max_databases_per_app = 5; // Max open databases
int max_query_time_ms = 5000; // 5 second query timeout
int max_result_rows = 10000; // Max rows returned
};
class DatabaseHandle;
class DatabaseManager {
public:
DatabaseManager(const std::string& app_id,
const std::string& app_root,
const DatabaseLimits& limits = DatabaseLimits{});
~DatabaseManager();
// Database operations
std::shared_ptr<DatabaseHandle> Open(const std::string& name, std::string& error);
void CloseAll();
// Stats
size_t GetOpenDatabaseCount() const;
private:
std::string m_app_id;
std::string m_app_root;
DatabaseLimits m_limits;
std::unordered_map<std::string, std::shared_ptr<DatabaseHandle>> m_databases;
std::string ResolvePath(const std::string& name);
bool ValidateName(const std::string& name, std::string& error);
};
class DatabaseHandle {
public:
DatabaseHandle(sqlite3* db, const std::string& path, const DatabaseLimits& limits);
~DatabaseHandle();
// Execute (INSERT, UPDATE, DELETE, CREATE, etc.)
bool Execute(const std::string& sql, const std::vector<SqlValue>& params, std::string& error);
// Query (SELECT)
std::optional<SqlResult> Query(const std::string& sql, const std::vector<SqlValue>& params,
std::string& error);
// Get last insert rowid
int64_t GetLastInsertRowId() const;
// Get affected rows
int GetChanges() const;
bool IsOpen() const { return m_db != nullptr; }
void Close();
private:
sqlite3* m_db;
std::string m_path;
DatabaseLimits m_limits;
static int Authorizer(void* user_data, int action, const char* arg1,
const char* arg2, const char* arg3, const char* arg4);
bool BindParameters(void* stmt, const std::vector<SqlValue>& params, std::string& error);
};
// Register database.* APIs as globals
void RegisterDatabaseAPI(lua_State* L, DatabaseManager* manager);
} // namespace mosis

View File

@@ -0,0 +1,55 @@
#pragma once
#include <string>
#include <vector>
#include <optional>
#include <cstdint>
namespace mosis {
struct ParsedUrl {
std::string scheme; // "https"
std::string host; // "api.example.com" or "192.0.2.1"
uint16_t port; // 443
std::string path; // "/api/data"
std::string query; // "?key=value"
bool is_ip_address; // true if host is IP literal
};
class HttpValidator {
public:
HttpValidator();
// Set allowed domains (from app manifest)
void SetAllowedDomains(const std::vector<std::string>& domains);
// Clear domain restrictions (for testing)
void ClearDomainRestrictions();
// Validate URL
// Returns parsed URL on success, sets error on failure
std::optional<ParsedUrl> Validate(const std::string& url, std::string& error);
private:
std::vector<std::string> m_allowed_domains;
bool m_domain_restrictions_enabled;
// IP address validation
bool IsIPv4Address(const std::string& host);
bool IsIPv6Address(const std::string& host);
bool IsPrivateIPv4(const std::string& ip);
bool IsPrivateIPv6(const std::string& ip);
bool IsLocalhostIP(const std::string& host);
bool IsMetadataIP(const std::string& host);
bool IsBlockedIP(const std::string& host);
// Domain validation
bool IsDomainAllowed(const std::string& host);
bool IsLocalhostName(const std::string& host);
bool IsMetadataHostname(const std::string& host);
// URL parsing
std::optional<ParsedUrl> ParseUrl(const std::string& url);
};
} // namespace mosis

View File

@@ -0,0 +1,22 @@
#pragma once
#include <string>
#include <cstddef>
struct lua_State;
namespace mosis {
// Configuration limits for JSON operations
struct JsonLimits {
int max_depth = 32; // Maximum nesting depth
size_t max_string_length = 1 * 1024 * 1024; // 1 MB per string
size_t max_output_size = 10 * 1024 * 1024; // 10 MB total output
size_t max_array_size = 100000; // Max elements in array
size_t max_object_size = 10000; // Max keys in object
};
// Register json.encode() and json.decode() as globals
void RegisterJsonAPI(lua_State* L, const JsonLimits& limits = JsonLimits{});
} // namespace mosis

View File

@@ -0,0 +1,101 @@
#pragma once
#include <string>
#include <vector>
#include <cstdint>
// Forward declare lua_State to avoid including lua.h in header
struct lua_State;
struct lua_Debug;
namespace mosis {
// Resource limits for sandbox
struct SandboxLimits {
size_t memory_bytes = 16 * 1024 * 1024; // 16 MB default
size_t max_string_size = 1 * 1024 * 1024; // 1 MB max string
size_t max_table_entries = 100000; // Prevent hash DoS
uint64_t instructions_per_call = 1000000; // ~10ms execution
int stack_depth = 200; // Recursion limit
};
// Context for sandbox (app identity, permissions, etc.)
struct SandboxContext {
std::string app_id;
std::string app_path;
std::vector<std::string> permissions;
bool is_system_app = false;
};
// Isolated Lua execution environment
class LuaSandbox {
public:
explicit LuaSandbox(const SandboxContext& context,
const SandboxLimits& limits = {});
~LuaSandbox();
// Non-copyable, non-movable
LuaSandbox(const LuaSandbox&) = delete;
LuaSandbox& operator=(const LuaSandbox&) = delete;
LuaSandbox(LuaSandbox&&) = delete;
LuaSandbox& operator=(LuaSandbox&&) = delete;
// Load and execute Lua code (text only, bytecode rejected)
bool LoadString(const std::string& code, const std::string& chunk_name = "chunk");
bool LoadFile(const std::string& path);
// State access
lua_State* GetState() const { return m_L; }
const std::string& GetLastError() const { return m_last_error; }
// Resource usage
size_t GetMemoryUsed() const { return m_memory_used; }
uint64_t GetInstructionsUsed() const { return m_instructions_used; }
// Context access
const SandboxContext& GetContext() const { return m_context; }
const SandboxLimits& GetLimits() const { return m_limits; }
const std::string& app_id() const { return m_context.app_id; }
// Reset instruction counter (call before each event handler)
void ResetInstructionCount();
// Check if sandbox is in valid state
bool IsValid() const { return m_L != nullptr; }
private:
// Setup functions
void SetupSandbox();
void RemoveDangerousGlobals();
void ProtectBuiltinTables();
void SetupInstructionHook();
void SetupSafeGlobals();
void SetupSafeRequire();
// Allocator callback (static for C compatibility)
static void* SandboxAlloc(void* ud, void* ptr, size_t osize, size_t nsize);
// Instruction hook callback (static for C compatibility)
static void InstructionHook(lua_State* L, lua_Debug* ar);
// Safe print function
static int SafePrint(lua_State* L);
// Safe require function
static int SafeRequire(lua_State* L);
lua_State* m_L = nullptr;
SandboxContext m_context;
SandboxLimits m_limits;
size_t m_memory_used = 0;
uint64_t m_instructions_used = 0;
std::string m_last_error;
};
} // namespace mosis
// Convenience alias for tests
using SandboxContext = mosis::SandboxContext;
using SandboxLimits = mosis::SandboxLimits;
using LuaSandbox = mosis::LuaSandbox;

View File

@@ -0,0 +1,76 @@
#pragma once
#include <string>
#include <vector>
#include <map>
#include <mutex>
#include <atomic>
#include "http_validator.h"
struct lua_State;
namespace mosis {
struct HttpRequest {
std::string url;
std::string method = "GET";
std::map<std::string, std::string> headers;
std::string body;
int timeout_ms = 30000;
};
struct HttpResponse {
int status_code = 0;
std::map<std::string, std::string> headers;
std::string body;
std::string error;
};
struct NetworkLimits {
size_t max_request_body = 10 * 1024 * 1024; // 10 MB
size_t max_response_body = 50 * 1024 * 1024; // 50 MB
int max_timeout_ms = 60000; // 60 seconds
int max_concurrent_requests = 6;
int default_timeout_ms = 30000;
};
class NetworkManager {
public:
NetworkManager(const std::string& app_id, const NetworkLimits& limits = NetworkLimits{});
~NetworkManager();
// Configure domain restrictions
void SetAllowedDomains(const std::vector<std::string>& domains);
void ClearDomainRestrictions();
// Synchronous request
// In test mode, validates but doesn't actually make network calls
HttpResponse Request(const HttpRequest& request, std::string& error);
// Stats
int GetActiveRequestCount() const;
// Access validator for testing
HttpValidator& GetValidator() { return m_validator; }
const HttpValidator& GetValidator() const { return m_validator; }
// For testing: set mock mode (no actual network calls)
void SetMockMode(bool enabled) { m_mock_mode = enabled; }
bool IsMockMode() const { return m_mock_mode; }
private:
std::string m_app_id;
NetworkLimits m_limits;
HttpValidator m_validator;
std::atomic<int> m_active_requests{0};
std::mutex m_mutex;
bool m_mock_mode = true; // Default to mock mode for tests
// Validate request before sending
bool ValidateRequest(const HttpRequest& request, std::string& error);
};
// Register network.* APIs as globals
void RegisterNetworkAPI(lua_State* L, NetworkManager* manager);
} // namespace mosis

View File

@@ -0,0 +1,52 @@
#pragma once
#include <string>
#include <filesystem>
struct lua_State;
namespace mosis {
class PathSandbox {
public:
explicit PathSandbox(const std::string& app_path);
// Validate a path is within the sandbox
// Returns true if valid, sets out_canonical to the resolved path
bool ValidatePath(const std::string& path, std::string& out_canonical);
// Check if path contains traversal attempts (..)
static bool ContainsTraversal(const std::string& path);
// Check if path is absolute
static bool IsAbsolutePath(const std::string& path);
// Normalize path separators and remove redundant ./ components
static std::string NormalizePath(const std::string& path);
// Validate module name for require() - alphanumeric, underscore, dots only
static bool IsValidModuleName(const std::string& name);
// Convert module name to relative path (e.g., "ui.button" -> "scripts/ui/button.lua")
static std::string ModuleToPath(const std::string& module_name);
// Get the app's base path
const std::string& GetAppPath() const { return m_app_path; }
// Resolve a relative path to full path within sandbox
std::string ResolvePath(const std::string& relative_path);
private:
std::string m_app_path;
};
// Safe require implementation for Lua
// Loads modules only from app_path/scripts/<module>.lua
// Caches modules in registry
int SafeRequire(lua_State* L);
// Register safe require as global "require"
// The PathSandbox pointer is stored in registry for use by SafeRequire
void RegisterSafeRequire(lua_State* L, PathSandbox* sandbox);
} // namespace mosis

View File

@@ -0,0 +1,73 @@
#pragma once
#include <string>
#include <vector>
#include <unordered_set>
#include <unordered_map>
#include <chrono>
struct lua_State;
namespace mosis {
struct SandboxContext; // Forward declaration
enum class PermissionCategory {
Normal, // Auto-granted when declared (e.g., internet, vibrate)
Dangerous, // Requires user consent (e.g., camera, location)
Signature // System apps only (e.g., system.settings)
};
struct PermissionInfo {
PermissionCategory category;
std::string description;
};
class PermissionGate {
public:
explicit PermissionGate(const SandboxContext& context);
// Check if app has permission (throws Lua error if not)
bool Check(lua_State* L, const std::string& permission);
// Check without throwing (returns false if denied)
bool HasPermission(const std::string& permission) const;
// Get permission category
static PermissionCategory GetCategory(const std::string& permission);
// Get permission info (returns nullptr if unknown)
static const PermissionInfo* GetPermissionInfo(const std::string& permission);
// User gesture tracking
void RecordUserGesture();
bool HasRecentUserGesture(int ms = 5000) const;
// Runtime permission grant (called after user consent)
void GrantPermission(const std::string& permission);
void RevokePermission(const std::string& permission);
// Get all declared permissions
const std::vector<std::string>& GetDeclaredPermissions() const;
// Get all granted permissions
std::vector<std::string> GetGrantedPermissions() const;
// Check if permission is declared in manifest
bool IsDeclared(const std::string& permission) const;
private:
const SandboxContext& m_context;
std::unordered_set<std::string> m_runtime_grants; // Runtime-granted dangerous perms
std::chrono::steady_clock::time_point m_last_gesture;
bool CheckNormalPermission(const std::string& permission) const;
bool CheckDangerousPermission(const std::string& permission) const;
bool CheckSignaturePermission(const std::string& permission) const;
};
} // namespace mosis
// Convenience alias
using PermissionGate = mosis::PermissionGate;
using PermissionCategory = mosis::PermissionCategory;

View File

@@ -0,0 +1,68 @@
#pragma once
#include <string>
#include <unordered_map>
#include <mutex>
#include <chrono>
namespace mosis {
struct RateLimitConfig {
double tokens_per_second; // Refill rate
double max_tokens; // Bucket capacity
};
class RateLimiter {
public:
// Default limits for common operations
RateLimiter();
// Check if operation is allowed (consumes token if yes)
bool Check(const std::string& app_id, const std::string& operation);
// Check without consuming token
bool CanProceed(const std::string& app_id, const std::string& operation) const;
// Configure limits for an operation
void SetLimit(const std::string& operation, const RateLimitConfig& config);
// Get config for an operation
const RateLimitConfig* GetLimit(const std::string& operation) const;
// Get current token count for app+operation
double GetTokens(const std::string& app_id, const std::string& operation) const;
// Reset all buckets for an app (e.g., on app restart)
void ResetApp(const std::string& app_id);
// Clear all buckets
void ClearAll();
private:
struct Bucket {
double tokens;
std::chrono::steady_clock::time_point last_refill;
};
// Refill bucket based on elapsed time
void Refill(Bucket& bucket, const RateLimitConfig& config) const;
// Get or create bucket for app+operation
Bucket& GetBucket(const std::string& app_id, const std::string& operation);
// Get bucket key
static std::string MakeKey(const std::string& app_id, const std::string& operation);
mutable std::mutex m_mutex;
std::unordered_map<std::string, RateLimitConfig> m_configs;
mutable std::unordered_map<std::string, Bucket> m_buckets;
};
// Global rate limiter (singleton)
RateLimiter& GetRateLimiter();
} // namespace mosis
// Convenience alias
using RateLimiter = mosis::RateLimiter;
using RateLimitConfig = mosis::RateLimitConfig;

View File

@@ -0,0 +1,87 @@
#pragma once
#include <string>
#include <functional>
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <chrono>
#include <mutex>
#include <cstdint>
struct lua_State;
namespace mosis {
using TimerId = uint64_t;
using TimePoint = std::chrono::steady_clock::time_point;
using Duration = std::chrono::milliseconds;
struct Timer {
TimerId id;
std::string app_id;
TimePoint fire_time;
Duration interval; // 0 for setTimeout, >0 for setInterval
int callback_ref; // Lua registry reference
lua_State* L; // Lua state that owns the callback
bool cancelled = false;
bool is_interval = false;
};
class TimerManager {
public:
TimerManager();
~TimerManager();
// Non-copyable
TimerManager(const TimerManager&) = delete;
TimerManager& operator=(const TimerManager&) = delete;
// Create timers (returns timer ID, 0 on failure)
TimerId SetTimeout(lua_State* L, const std::string& app_id,
int callback_ref, int delay_ms);
TimerId SetInterval(lua_State* L, const std::string& app_id,
int callback_ref, int interval_ms);
// Cancel timers
bool ClearTimer(const std::string& app_id, TimerId id);
// Cancel all timers for an app (call on app stop)
void ClearAppTimers(const std::string& app_id);
// Process timers (call from main loop)
// Returns number of timers fired
int ProcessTimers();
// Get timer count for an app
size_t GetTimerCount(const std::string& app_id) const;
// Configuration
static constexpr size_t MAX_TIMERS_PER_APP = 100;
static constexpr int MIN_INTERVAL_MS = 10;
static constexpr int MIN_TIMEOUT_MS = 0;
private:
TimerId m_next_id = 1;
// All timers (we use a vector and sort/search as needed)
std::vector<Timer> m_timers;
// Track timer count per app
std::unordered_map<std::string, size_t> m_app_timer_counts;
// Track which timer IDs belong to which app (for fast cancellation)
std::unordered_map<std::string, std::unordered_set<TimerId>> m_app_timer_ids;
mutable std::mutex m_mutex;
void FireTimer(Timer& timer);
void RemoveTimer(TimerId id);
void RescheduleInterval(Timer& timer);
};
// Lua API registration
// Registers: setTimeout, clearTimeout, setInterval, clearInterval
void RegisterTimerAPI(lua_State* L, TimerManager* manager, const std::string& app_id);
} // namespace mosis

View File

@@ -0,0 +1,77 @@
#pragma once
#include <string>
#include <vector>
#include <cstdint>
#include <optional>
#include <functional>
struct lua_State;
namespace mosis {
struct FileStat {
size_t size;
int64_t modified; // Unix timestamp
bool is_dir;
};
struct VirtualFSLimits {
size_t max_quota_bytes = 50 * 1024 * 1024; // 50 MB per app
size_t max_file_size = 10 * 1024 * 1024; // 10 MB per file
int max_path_depth = 10; // Max directory depth
size_t max_path_length = 256; // Max path string length
};
class VirtualFS {
public:
VirtualFS(const std::string& app_id,
const std::string& app_root,
const VirtualFSLimits& limits = VirtualFSLimits{});
~VirtualFS();
// Path operations
bool ValidatePath(const std::string& virtual_path, std::string& error);
std::string ResolvePath(const std::string& virtual_path);
// File operations
std::optional<std::string> Read(const std::string& path, std::string& error);
bool Write(const std::string& path, const std::string& data, std::string& error);
bool Append(const std::string& path, const std::string& data, std::string& error);
bool Delete(const std::string& path, std::string& error);
bool Exists(const std::string& path);
std::optional<std::vector<std::string>> List(const std::string& path, std::string& error);
bool MakeDir(const std::string& path, std::string& error);
std::optional<FileStat> Stat(const std::string& path, std::string& error);
// Quota management
size_t GetUsedBytes() const { return m_used_bytes; }
size_t GetQuotaBytes() const { return m_limits.max_quota_bytes; }
void RecalculateUsage();
// Cleanup
void ClearTemp();
void ClearAll(); // For testing
// Permission check callback (set by sandbox)
std::function<bool(const std::string&)> CheckPermission;
private:
std::string m_app_id;
std::string m_app_root;
VirtualFSLimits m_limits;
size_t m_used_bytes = 0;
bool EnsureParentDir(const std::string& path);
void UpdateUsage(int64_t delta);
bool CheckQuota(size_t additional_bytes, std::string& error);
int GetPathDepth(const std::string& path);
bool IsValidPathChar(char c);
void DeleteDirectoryRecursive(const std::string& path);
size_t CalculateDirectorySize(const std::string& path);
};
// Register fs.* APIs as globals
void RegisterVirtualFS(lua_State* L, VirtualFS* vfs);
} // namespace mosis

View File

@@ -0,0 +1,40 @@
#pragma once
#include <string>
#include <cstdio>
#include <cstdarg>
class Logger
{
public:
static void Log(const std::string& message);
// Printf-style logging
static void LogF(const char* level, const char* fmt, ...) {
char buffer[1024];
va_list args;
va_start(args, fmt);
vsnprintf(buffer, sizeof(buffer), fmt, args);
va_end(args);
Log(std::string("[") + level + "] " + buffer);
}
};
// Undefine conflicting syslog macros if present
#ifdef LOG_DEBUG
#undef LOG_DEBUG
#endif
#ifdef LOG_INFO
#undef LOG_INFO
#endif
#ifdef LOG_WARN
#undef LOG_WARN
#endif
#ifdef LOG_ERROR
#undef LOG_ERROR
#endif
// Logging macros for convenience (printf-style)
#define LOG_DEBUG(fmt, ...) Logger::LogF("DEBUG", fmt __VA_OPT__(,) __VA_ARGS__)
#define LOG_INFO(fmt, ...) Logger::LogF("INFO", fmt __VA_OPT__(,) __VA_ARGS__)
#define LOG_WARN(fmt, ...) Logger::LogF("WARN", fmt __VA_OPT__(,) __VA_ARGS__)
#define LOG_ERROR(fmt, ...) Logger::LogF("ERROR", fmt __VA_OPT__(,) __VA_ARGS__)

539
core/src/apps/app_api.cpp Normal file
View File

@@ -0,0 +1,539 @@
// app_api.cpp - Lua API bindings for app management implementation
// Milestone 10: Device-Side App Management
#include "app_api.h"
#include "app_manager.h"
#include "update_service.h"
#include "../logger.h"
#include <lua.hpp>
#include <string>
#include <chrono>
namespace mosis {
// Registry keys for storing pointers
static const char* APP_MANAGER_KEY = "mosis.app_manager";
static const char* UPDATE_SERVICE_KEY = "mosis.update_service";
static const char* CURRENT_APP_ID_KEY = "mosis.current_app_id";
static const char* IS_SYSTEM_APP_KEY = "mosis.is_system_app";
// Helper to get AppManager from Lua registry
static AppManager* GetAppManager(lua_State* L) {
lua_getfield(L, LUA_REGISTRYINDEX, APP_MANAGER_KEY);
auto* manager = static_cast<AppManager*>(lua_touserdata(L, -1));
lua_pop(L, 1);
return manager;
}
// Helper to get UpdateService from Lua registry
static UpdateService* GetUpdateService(lua_State* L) {
lua_getfield(L, LUA_REGISTRYINDEX, UPDATE_SERVICE_KEY);
auto* service = static_cast<UpdateService*>(lua_touserdata(L, -1));
lua_pop(L, 1);
return service;
}
// Helper to get current app ID
static std::string GetCurrentAppId(lua_State* L) {
lua_getfield(L, LUA_REGISTRYINDEX, CURRENT_APP_ID_KEY);
std::string id = lua_tostring(L, -1) ? lua_tostring(L, -1) : "";
lua_pop(L, 1);
return id;
}
// Helper to check if system app
static bool IsSystemApp(lua_State* L) {
lua_getfield(L, LUA_REGISTRYINDEX, IS_SYSTEM_APP_KEY);
bool is_system = lua_toboolean(L, -1);
lua_pop(L, 1);
return is_system;
}
// ============================================================================
// mosis.apps.* - System apps only
// ============================================================================
// mosis.apps.getInstalled() -> [{package_id, name, version_name, version_code, installed_at}]
static int apps_getInstalled(lua_State* L) {
if (!IsSystemApp(L)) {
return luaL_error(L, "mosis.apps.getInstalled requires system permission");
}
auto* manager = GetAppManager(L);
if (!manager) {
lua_newtable(L);
return 1;
}
auto apps = manager->GetInstalledApps();
lua_createtable(L, static_cast<int>(apps.size()), 0);
int idx = 1;
for (const auto& app : apps) {
lua_createtable(L, 0, 8);
lua_pushstring(L, app.package_id.c_str());
lua_setfield(L, -2, "package_id");
lua_pushstring(L, app.name.c_str());
lua_setfield(L, -2, "name");
lua_pushstring(L, app.version_name.c_str());
lua_setfield(L, -2, "version_name");
lua_pushinteger(L, app.version_code);
lua_setfield(L, -2, "version_code");
lua_pushboolean(L, app.is_system_app);
lua_setfield(L, -2, "is_system_app");
lua_pushstring(L, app.icon_path.c_str());
lua_setfield(L, -2, "icon");
lua_pushstring(L, app.developer_name.c_str());
lua_setfield(L, -2, "developer");
// installed_at as Unix timestamp
auto ts = std::chrono::duration_cast<std::chrono::seconds>(
app.installed_at.time_since_epoch()).count();
lua_pushinteger(L, ts);
lua_setfield(L, -2, "installed_at");
lua_rawseti(L, -2, idx++);
}
return 1;
}
// mosis.apps.getInfo(package_id) -> {info} or nil
static int apps_getInfo(lua_State* L) {
if (!IsSystemApp(L)) {
return luaL_error(L, "mosis.apps.getInfo requires system permission");
}
const char* package_id = luaL_checkstring(L, 1);
auto* manager = GetAppManager(L);
if (!manager) {
lua_pushnil(L);
return 1;
}
auto app = manager->GetApp(package_id);
if (!app) {
lua_pushnil(L);
return 1;
}
lua_createtable(L, 0, 12);
lua_pushstring(L, app->package_id.c_str());
lua_setfield(L, -2, "package_id");
lua_pushstring(L, app->name.c_str());
lua_setfield(L, -2, "name");
lua_pushstring(L, app->version_name.c_str());
lua_setfield(L, -2, "version_name");
lua_pushinteger(L, app->version_code);
lua_setfield(L, -2, "version_code");
lua_pushboolean(L, app->is_system_app);
lua_setfield(L, -2, "is_system_app");
lua_pushinteger(L, app->package_size);
lua_setfield(L, -2, "package_size");
lua_pushinteger(L, app->data_size);
lua_setfield(L, -2, "data_size");
lua_pushstring(L, app->icon_path.c_str());
lua_setfield(L, -2, "icon");
lua_pushstring(L, app->developer_name.c_str());
lua_setfield(L, -2, "developer");
// Permissions array
lua_createtable(L, static_cast<int>(app->permissions.size()), 0);
int idx = 1;
for (const auto& perm : app->permissions) {
lua_pushstring(L, perm.c_str());
lua_rawseti(L, -2, idx++);
}
lua_setfield(L, -2, "permissions");
return 1;
}
// mosis.apps.install(url, signature, callback)
static int apps_install(lua_State* L) {
if (!IsSystemApp(L)) {
return luaL_error(L, "mosis.apps.install requires system permission");
}
const char* url = luaL_checkstring(L, 1);
const char* signature = lua_isstring(L, 2) ? lua_tostring(L, 2) : "";
// Callback is optional (argument 3)
bool has_callback = lua_isfunction(L, 3);
auto* manager = GetAppManager(L);
if (!manager) {
lua_pushboolean(L, false);
return 1;
}
if (has_callback) {
// Store callback reference
lua_pushvalue(L, 3);
int callback_ref = luaL_ref(L, LUA_REGISTRYINDEX);
// Create progress callback that calls Lua
// Note: This is simplified - real implementation needs thread safety
ProgressCallback progress_cb = [L, callback_ref](const InstallProgress& progress) {
lua_rawgeti(L, LUA_REGISTRYINDEX, callback_ref);
lua_createtable(L, 0, 3);
lua_pushstring(L, InstallProgress::StageName(progress.stage));
lua_setfield(L, -2, "stage");
lua_pushnumber(L, progress.progress);
lua_setfield(L, -2, "progress");
lua_pushstring(L, progress.error.c_str());
lua_setfield(L, -2, "error");
if (lua_pcall(L, 1, 0, 0) != LUA_OK) {
LOG_ERROR("Install callback error: %s", lua_tostring(L, -1));
lua_pop(L, 1);
}
// Clean up ref when complete
if (progress.stage == InstallProgress::Stage::Complete ||
progress.stage == InstallProgress::Stage::Failed) {
luaL_unref(L, LUA_REGISTRYINDEX, callback_ref);
}
};
bool success = manager->Install(url, signature, progress_cb);
lua_pushboolean(L, success);
} else {
bool success = manager->Install(url, signature, nullptr);
lua_pushboolean(L, success);
}
return 1;
}
// mosis.apps.uninstall(package_id) -> boolean
static int apps_uninstall(lua_State* L) {
if (!IsSystemApp(L)) {
return luaL_error(L, "mosis.apps.uninstall requires system permission");
}
const char* package_id = luaL_checkstring(L, 1);
auto* manager = GetAppManager(L);
if (!manager) {
lua_pushboolean(L, false);
return 1;
}
bool success = manager->Uninstall(package_id);
lua_pushboolean(L, success);
return 1;
}
// mosis.apps.launch(package_id) -> boolean
static int apps_launch(lua_State* L) {
if (!IsSystemApp(L)) {
return luaL_error(L, "mosis.apps.launch requires system permission");
}
const char* package_id = luaL_checkstring(L, 1);
auto* manager = GetAppManager(L);
if (!manager) {
lua_pushboolean(L, false);
return 1;
}
bool success = manager->LaunchApp(package_id);
lua_pushboolean(L, success);
return 1;
}
// mosis.apps.getDataSize(package_id) -> number
static int apps_getDataSize(lua_State* L) {
if (!IsSystemApp(L)) {
return luaL_error(L, "mosis.apps.getDataSize requires system permission");
}
const char* package_id = luaL_checkstring(L, 1);
auto* manager = GetAppManager(L);
if (!manager) {
lua_pushinteger(L, 0);
return 1;
}
int64_t size = manager->GetAppDataSize(package_id);
lua_pushinteger(L, size);
return 1;
}
// mosis.apps.clearCache(package_id) -> boolean
static int apps_clearCache(lua_State* L) {
if (!IsSystemApp(L)) {
return luaL_error(L, "mosis.apps.clearCache requires system permission");
}
const char* package_id = luaL_checkstring(L, 1);
auto* manager = GetAppManager(L);
if (!manager) {
lua_pushboolean(L, false);
return 1;
}
bool success = manager->ClearAppCache(package_id);
lua_pushboolean(L, success);
return 1;
}
// mosis.apps.clearData(package_id) -> boolean
static int apps_clearData(lua_State* L) {
if (!IsSystemApp(L)) {
return luaL_error(L, "mosis.apps.clearData requires system permission");
}
const char* package_id = luaL_checkstring(L, 1);
auto* manager = GetAppManager(L);
if (!manager) {
lua_pushboolean(L, false);
return 1;
}
bool success = manager->ClearAppData(package_id);
lua_pushboolean(L, success);
return 1;
}
// mosis.apps.checkUpdates(callback) -> void
static int apps_checkUpdates(lua_State* L) {
if (!IsSystemApp(L)) {
return luaL_error(L, "mosis.apps.checkUpdates requires system permission");
}
auto* service = GetUpdateService(L);
if (!service) {
lua_pushnil(L);
return 1;
}
auto updates = service->CheckForUpdates();
lua_createtable(L, static_cast<int>(updates.size()), 0);
int idx = 1;
for (const auto& update : updates) {
lua_createtable(L, 0, 8);
lua_pushstring(L, update.package_id.c_str());
lua_setfield(L, -2, "package_id");
lua_pushstring(L, update.name.c_str());
lua_setfield(L, -2, "name");
lua_pushstring(L, update.current_version.c_str());
lua_setfield(L, -2, "current_version");
lua_pushstring(L, update.new_version.c_str());
lua_setfield(L, -2, "new_version");
lua_pushinteger(L, update.download_size);
lua_setfield(L, -2, "size");
lua_pushstring(L, update.release_notes.c_str());
lua_setfield(L, -2, "release_notes");
lua_pushboolean(L, update.is_critical);
lua_setfield(L, -2, "critical");
lua_rawseti(L, -2, idx++);
}
return 1;
}
// ============================================================================
// mosis.app.* - All apps (info about current app)
// ============================================================================
// mosis.app.info() -> {package_id, name, version_name, version_code}
static int app_info(lua_State* L) {
std::string app_id = GetCurrentAppId(L);
if (app_id.empty()) {
lua_pushnil(L);
return 1;
}
auto* manager = GetAppManager(L);
if (!manager) {
lua_pushnil(L);
return 1;
}
auto app = manager->GetApp(app_id);
if (!app) {
lua_pushnil(L);
return 1;
}
lua_createtable(L, 0, 4);
lua_pushstring(L, app->package_id.c_str());
lua_setfield(L, -2, "package_id");
lua_pushstring(L, app->name.c_str());
lua_setfield(L, -2, "name");
lua_pushstring(L, app->version_name.c_str());
lua_setfield(L, -2, "version_name");
lua_pushinteger(L, app->version_code);
lua_setfield(L, -2, "version_code");
return 1;
}
// mosis.app.checkUpdate(callback) -> void
static int app_checkUpdate(lua_State* L) {
// Get callback
if (!lua_isfunction(L, 1)) {
return luaL_error(L, "callback function required");
}
std::string app_id = GetCurrentAppId(L);
auto* service = GetUpdateService(L);
if (app_id.empty() || !service) {
// Call callback with no update
lua_pushvalue(L, 1);
lua_pushboolean(L, false);
lua_pushnil(L);
lua_pcall(L, 2, 0, 0);
return 0;
}
auto updates = service->GetPendingUpdates();
for (const auto& update : updates) {
if (update.package_id == app_id) {
// Call callback with update info
lua_pushvalue(L, 1);
lua_pushboolean(L, true);
lua_pushstring(L, update.new_version.c_str());
lua_pcall(L, 2, 0, 0);
return 0;
}
}
// No update available
lua_pushvalue(L, 1);
lua_pushboolean(L, false);
lua_pushnil(L);
lua_pcall(L, 2, 0, 0);
return 0;
}
// mosis.app.openStorePage() -> void
static int app_openStorePage(lua_State* L) {
std::string app_id = GetCurrentAppId(L);
if (app_id.empty()) {
return 0;
}
// TODO: Navigate to store page for this app
// This would typically trigger navigation to:
// mosis://store/app/{app_id}
LOG_INFO("Open store page for: %s", app_id.c_str());
return 0;
}
// ============================================================================
// Registration
// ============================================================================
static const luaL_Reg apps_functions[] = {
{"getInstalled", apps_getInstalled},
{"getInfo", apps_getInfo},
{"install", apps_install},
{"uninstall", apps_uninstall},
{"launch", apps_launch},
{"getDataSize", apps_getDataSize},
{"clearCache", apps_clearCache},
{"clearData", apps_clearData},
{"checkUpdates", apps_checkUpdates},
{nullptr, nullptr}
};
static const luaL_Reg app_functions[] = {
{"info", app_info},
{"checkUpdate", app_checkUpdate},
{"openStorePage", app_openStorePage},
{nullptr, nullptr}
};
void RegisterAppAPIs(lua_State* L,
AppManager* app_manager,
UpdateService* update_service,
const std::string& current_app_id,
bool is_system_app) {
// Store pointers in registry
lua_pushlightuserdata(L, app_manager);
lua_setfield(L, LUA_REGISTRYINDEX, APP_MANAGER_KEY);
lua_pushlightuserdata(L, update_service);
lua_setfield(L, LUA_REGISTRYINDEX, UPDATE_SERVICE_KEY);
lua_pushstring(L, current_app_id.c_str());
lua_setfield(L, LUA_REGISTRYINDEX, CURRENT_APP_ID_KEY);
lua_pushboolean(L, is_system_app);
lua_setfield(L, LUA_REGISTRYINDEX, IS_SYSTEM_APP_KEY);
// Get or create mosis table
lua_getglobal(L, "mosis");
if (lua_isnil(L, -1)) {
lua_pop(L, 1);
lua_newtable(L);
lua_setglobal(L, "mosis");
lua_getglobal(L, "mosis");
}
// Create mosis.apps table (system apps only)
if (is_system_app) {
lua_newtable(L);
luaL_setfuncs(L, apps_functions, 0);
lua_setfield(L, -2, "apps");
}
// Create mosis.app table (all apps)
lua_newtable(L);
luaL_setfuncs(L, app_functions, 0);
lua_setfield(L, -2, "app");
lua_pop(L, 1); // pop mosis table
LOG_DEBUG("Registered app APIs for: %s (system=%d)",
current_app_id.c_str(), is_system_app);
}
} // namespace mosis

View File

@@ -0,0 +1,697 @@
// app_manager.cpp - App installation and management implementation
// Milestone 10: Device-Side App Management
#include "app_manager.h"
#include "../logger.h"
#include "../sandbox/sandbox_manager.h"
#include <fstream>
#include <sstream>
#include <filesystem>
#include <random>
#include <iomanip>
#include <ctime>
// For JSON parsing
#include <nlohmann/json.hpp>
// For ZIP extraction
#include <minizip/unzip.h>
namespace fs = std::filesystem;
using json = nlohmann::json;
namespace mosis {
AppManager::AppManager(const std::string& data_root)
: m_data_root(data_root)
{
// Create directory structure
fs::create_directories(m_data_root + "/apps");
fs::create_directories(m_data_root + "/downloads");
fs::create_directories(m_data_root + "/backups");
fs::create_directories(m_data_root + "/config");
// Load installed apps registry
LoadInstalledApps();
LOG_INFO("AppManager initialized at: %s", m_data_root.c_str());
}
AppManager::~AppManager() {
SaveInstalledApps();
}
bool AppManager::Install(const std::string& package_url,
const std::string& signature,
ProgressCallback callback) {
callback({InstallProgress::Stage::Downloading, 0.0f, ""});
// Generate download path
std::string download_path = m_data_root + "/downloads/" + GenerateUUID() + ".mosis";
// Download package
if (!DownloadFile(package_url, download_path, [&](float p) {
callback({InstallProgress::Stage::Downloading, p, ""});
})) {
callback({InstallProgress::Stage::Failed, 0.0f, "Download failed"});
return false;
}
callback({InstallProgress::Stage::Verifying, 0.0f, ""});
// Verify signature
if (!signature.empty() && !VerifySignature(download_path, signature)) {
fs::remove(download_path);
callback({InstallProgress::Stage::Failed, 0.0f, "Signature verification failed"});
return false;
}
// Verify package integrity
if (!VerifyPackage(download_path)) {
fs::remove(download_path);
callback({InstallProgress::Stage::Failed, 0.0f, "Package verification failed"});
return false;
}
// Continue with installation
bool result = InstallFromFile(download_path, callback);
// Clean up download
fs::remove(download_path);
return result;
}
bool AppManager::InstallFromFile(const std::string& package_path,
ProgressCallback callback) {
callback({InstallProgress::Stage::Verifying, 0.0f, ""});
// Extract manifest to get package_id
auto manifest = ExtractManifest(package_path);
if (!manifest) {
callback({InstallProgress::Stage::Failed, 0.0f, "Invalid manifest"});
return false;
}
LOG_INFO("Installing app: %s v%s", manifest->id.c_str(), manifest->version.c_str());
callback({InstallProgress::Stage::Extracting, 0.0f, ""});
// Determine installation path
std::string install_path = m_data_root + "/apps/" + manifest->id;
// Check if already installed (update path)
if (fs::exists(install_path + "/package")) {
LOG_INFO("App already installed, updating: %s", manifest->id.c_str());
// Backup existing data
BackupAppData(manifest->id);
// Remove old package
fs::remove_all(install_path + "/package");
}
// Create directories
fs::create_directories(install_path + "/package");
fs::create_directories(install_path + "/data");
fs::create_directories(install_path + "/cache");
fs::create_directories(install_path + "/db");
// Extract package
if (!ExtractPackage(package_path, install_path + "/package")) {
callback({InstallProgress::Stage::Failed, 0.0f, "Extraction failed"});
return false;
}
callback({InstallProgress::Stage::Registering, 0.0f, ""});
// Get package file size
int64_t package_size = 0;
try {
package_size = static_cast<int64_t>(fs::file_size(package_path));
} catch (...) {
package_size = 0;
}
// Register app
InstalledApp app;
app.package_id = manifest->id;
app.name = manifest->name;
app.version_name = manifest->version;
app.version_code = manifest->version_code;
app.install_path = install_path;
app.permissions = manifest->permissions;
app.installed_at = std::chrono::system_clock::now();
app.updated_at = std::chrono::system_clock::now();
app.package_size = package_size;
app.data_size = 0;
app.is_system_app = false;
app.entry_point = manifest->entry;
app.icon_path = manifest->icon;
app.developer_name = manifest->developer_name;
{
std::lock_guard<std::mutex> lock(m_mutex);
m_installed_apps[manifest->id] = app;
SaveInstalledApps();
}
LOG_INFO("App installed successfully: %s", manifest->id.c_str());
callback({InstallProgress::Stage::Complete, 1.0f, ""});
return true;
}
bool AppManager::Uninstall(const std::string& package_id, bool keep_data) {
std::lock_guard<std::mutex> lock(m_mutex);
auto it = m_installed_apps.find(package_id);
if (it == m_installed_apps.end()) {
LOG_WARN("Cannot uninstall: app not found: %s", package_id.c_str());
return false;
}
// Cannot uninstall system apps
if (it->second.is_system_app) {
LOG_WARN("Cannot uninstall system app: %s", package_id.c_str());
return false;
}
LOG_INFO("Uninstalling app: %s (keep_data=%d)", package_id.c_str(), keep_data);
// Stop app if running
if (m_sandbox_manager && m_sandbox_manager->IsAppRunning(package_id)) {
LOG_INFO("Stopping running app before uninstall: %s", package_id.c_str());
m_sandbox_manager->StopApp(package_id);
}
// Remove files
std::string install_path = it->second.install_path;
try {
fs::remove_all(install_path + "/package");
fs::remove_all(install_path + "/cache");
if (!keep_data) {
fs::remove_all(install_path + "/data");
fs::remove_all(install_path + "/db");
fs::remove_all(install_path);
}
} catch (const std::exception& e) {
LOG_ERROR("Error removing app files: %s", e.what());
return false;
}
// Unregister
m_installed_apps.erase(it);
SaveInstalledApps();
LOG_INFO("App uninstalled: %s", package_id.c_str());
return true;
}
bool AppManager::Update(const std::string& package_id,
const std::string& package_url,
const std::string& signature,
ProgressCallback callback) {
// Updates use the same flow as Install, which handles existing installations
return Install(package_url, signature, callback);
}
std::vector<InstalledApp> AppManager::GetInstalledApps() const {
std::lock_guard<std::mutex> lock(m_mutex);
std::vector<InstalledApp> apps;
apps.reserve(m_installed_apps.size());
for (const auto& [id, app] : m_installed_apps) {
apps.push_back(app);
}
return apps;
}
std::optional<InstalledApp> AppManager::GetApp(const std::string& package_id) const {
std::lock_guard<std::mutex> lock(m_mutex);
auto it = m_installed_apps.find(package_id);
if (it != m_installed_apps.end()) {
return it->second;
}
return std::nullopt;
}
bool AppManager::IsInstalled(const std::string& package_id) const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_installed_apps.find(package_id) != m_installed_apps.end();
}
int64_t AppManager::GetAppDataSize(const std::string& package_id) const {
std::string data_path = GetAppDataPath(package_id);
return CalculateDirectorySize(data_path);
}
bool AppManager::ClearAppData(const std::string& package_id) {
std::string data_path = GetAppDataPath(package_id);
std::string db_path = m_data_root + "/apps/" + package_id + "/db";
try {
fs::remove_all(data_path);
fs::remove_all(db_path);
fs::create_directories(data_path);
fs::create_directories(db_path);
LOG_INFO("Cleared app data: %s", package_id.c_str());
return true;
} catch (const std::exception& e) {
LOG_ERROR("Error clearing app data: %s", e.what());
return false;
}
}
bool AppManager::ClearAppCache(const std::string& package_id) {
std::string cache_path = GetAppCachePath(package_id);
try {
fs::remove_all(cache_path);
fs::create_directories(cache_path);
LOG_INFO("Cleared app cache: %s", package_id.c_str());
return true;
} catch (const std::exception& e) {
LOG_ERROR("Error clearing app cache: %s", e.what());
return false;
}
}
bool AppManager::BackupAppData(const std::string& package_id) {
std::string data_path = GetAppDataPath(package_id);
std::string backup_path = m_data_root + "/backups/" + package_id;
try {
if (fs::exists(data_path)) {
fs::remove_all(backup_path);
fs::copy(data_path, backup_path, fs::copy_options::recursive);
LOG_INFO("Backed up app data: %s", package_id.c_str());
}
return true;
} catch (const std::exception& e) {
LOG_ERROR("Error backing up app data: %s", e.what());
return false;
}
}
bool AppManager::RestoreAppData(const std::string& package_id) {
std::string data_path = GetAppDataPath(package_id);
std::string backup_path = m_data_root + "/backups/" + package_id;
try {
if (fs::exists(backup_path)) {
fs::remove_all(data_path);
fs::copy(backup_path, data_path, fs::copy_options::recursive);
LOG_INFO("Restored app data: %s", package_id.c_str());
}
return true;
} catch (const std::exception& e) {
LOG_ERROR("Error restoring app data: %s", e.what());
return false;
}
}
bool AppManager::LaunchApp(const std::string& package_id) {
auto app = GetApp(package_id);
if (!app) {
LOG_ERROR("Cannot launch app: not installed: %s", package_id.c_str());
return false;
}
if (!m_sandbox_manager) {
LOG_ERROR("Cannot launch app: sandbox manager not set");
return false;
}
if (m_sandbox_manager->IsAppRunning(package_id)) {
LOG_WARN("App already running: %s", package_id.c_str());
return true;
}
std::string app_path = app->install_path + "/package";
LOG_INFO("Launching app: %s from %s", package_id.c_str(), app_path.c_str());
return m_sandbox_manager->StartApp(package_id, app_path, app->permissions, app->is_system_app);
}
bool AppManager::StopApp(const std::string& package_id) {
if (!m_sandbox_manager) {
LOG_ERROR("Cannot stop app: sandbox manager not set");
return false;
}
if (!m_sandbox_manager->IsAppRunning(package_id)) {
LOG_WARN("App not running: %s", package_id.c_str());
return true;
}
LOG_INFO("Stopping app: %s", package_id.c_str());
return m_sandbox_manager->StopApp(package_id);
}
bool AppManager::IsAppRunning(const std::string& package_id) const {
if (!m_sandbox_manager) {
return false;
}
return m_sandbox_manager->IsAppRunning(package_id);
}
void AppManager::SetSandboxManager(LuaSandboxManager* manager) {
m_sandbox_manager = manager;
}
std::string AppManager::GetAppPath(const std::string& package_id) const {
return m_data_root + "/apps/" + package_id + "/package";
}
std::string AppManager::GetAppDataPath(const std::string& package_id) const {
return m_data_root + "/apps/" + package_id + "/data";
}
std::string AppManager::GetAppCachePath(const std::string& package_id) const {
return m_data_root + "/apps/" + package_id + "/cache";
}
void AppManager::RegisterSystemApp(const InstalledApp& app) {
std::lock_guard<std::mutex> lock(m_mutex);
InstalledApp system_app = app;
system_app.is_system_app = true;
m_installed_apps[app.package_id] = system_app;
LOG_INFO("Registered system app: %s", app.package_id.c_str());
}
bool AppManager::VerifyPackage(const std::string& path) {
// Verify ZIP structure and manifest presence
unzFile zip = unzOpen(path.c_str());
if (!zip) {
LOG_ERROR("Cannot open package: %s", path.c_str());
return false;
}
bool has_manifest = false;
if (unzGoToFirstFile(zip) == UNZ_OK) {
do {
char filename[256];
unz_file_info file_info;
if (unzGetCurrentFileInfo(zip, &file_info, filename, sizeof(filename),
nullptr, 0, nullptr, 0) == UNZ_OK) {
if (std::string(filename) == "manifest.json") {
has_manifest = true;
break;
}
}
} while (unzGoToNextFile(zip) == UNZ_OK);
}
unzClose(zip);
if (!has_manifest) {
LOG_ERROR("Package missing manifest.json: %s", path.c_str());
return false;
}
return true;
}
bool AppManager::VerifySignature(const std::string& path, const std::string& signature) {
// TODO: Implement Ed25519 signature verification
// For now, accept packages without strict verification
LOG_WARN("Signature verification not yet implemented");
return true;
}
std::optional<AppManifest> AppManager::ExtractManifest(const std::string& package_path) {
unzFile zip = unzOpen(package_path.c_str());
if (!zip) {
return std::nullopt;
}
std::string manifest_content;
// Find and read manifest.json
if (unzLocateFile(zip, "manifest.json", 0) != UNZ_OK) {
unzClose(zip);
return std::nullopt;
}
if (unzOpenCurrentFile(zip) != UNZ_OK) {
unzClose(zip);
return std::nullopt;
}
char buffer[4096];
int bytes_read;
while ((bytes_read = unzReadCurrentFile(zip, buffer, sizeof(buffer))) > 0) {
manifest_content.append(buffer, bytes_read);
}
unzCloseCurrentFile(zip);
unzClose(zip);
// Parse JSON
try {
json j = json::parse(manifest_content);
AppManifest manifest;
manifest.id = j.value("id", "");
manifest.name = j.value("name", "");
manifest.version = j.value("version", "1.0.0");
manifest.version_code = j.value("version_code", 1);
manifest.entry = j.value("entry", "main.rml");
manifest.icon = j.value("icon", "");
manifest.description = j.value("description", "");
manifest.min_api_version = j.value("min_api_version", 1);
if (j.contains("developer")) {
manifest.developer_name = j["developer"].value("name", "");
manifest.developer_email = j["developer"].value("email", "");
}
if (j.contains("permissions") && j["permissions"].is_array()) {
for (const auto& perm : j["permissions"]) {
manifest.permissions.push_back(perm.get<std::string>());
}
}
if (manifest.id.empty()) {
LOG_ERROR("Manifest missing required 'id' field");
return std::nullopt;
}
return manifest;
} catch (const json::exception& e) {
LOG_ERROR("Failed to parse manifest: %s", e.what());
return std::nullopt;
}
}
bool AppManager::ExtractPackage(const std::string& package_path, const std::string& dest_path) {
unzFile zip = unzOpen(package_path.c_str());
if (!zip) {
LOG_ERROR("Cannot open package for extraction: %s", package_path.c_str());
return false;
}
bool success = true;
if (unzGoToFirstFile(zip) == UNZ_OK) {
do {
char filename[512];
unz_file_info file_info;
if (unzGetCurrentFileInfo(zip, &file_info, filename, sizeof(filename),
nullptr, 0, nullptr, 0) != UNZ_OK) {
continue;
}
std::string full_path = dest_path + "/" + filename;
// Skip META-INF directory (signatures)
if (std::string(filename).rfind("META-INF/", 0) == 0) {
continue;
}
// Create directories
size_t len = strlen(filename);
if (len > 0 && filename[len - 1] == '/') {
fs::create_directories(full_path);
continue;
}
// Ensure parent directory exists
fs::create_directories(fs::path(full_path).parent_path());
// Extract file
if (unzOpenCurrentFile(zip) != UNZ_OK) {
LOG_ERROR("Cannot open file in archive: %s", filename);
success = false;
break;
}
std::ofstream out(full_path, std::ios::binary);
if (!out) {
LOG_ERROR("Cannot create file: %s", full_path.c_str());
unzCloseCurrentFile(zip);
success = false;
break;
}
char buffer[8192];
int bytes_read;
while ((bytes_read = unzReadCurrentFile(zip, buffer, sizeof(buffer))) > 0) {
out.write(buffer, bytes_read);
}
out.close();
unzCloseCurrentFile(zip);
} while (unzGoToNextFile(zip) == UNZ_OK);
}
unzClose(zip);
return success;
}
bool AppManager::DownloadFile(const std::string& url, const std::string& dest_path,
std::function<void(float)> progress_callback) {
// TODO: Implement HTTP download using platform-specific APIs
// For now, return false as this is a placeholder
LOG_ERROR("HTTP download not yet implemented for: %s", url.c_str());
return false;
}
void AppManager::LoadInstalledApps() {
std::string registry_path = m_data_root + "/config/apps.json";
std::ifstream file(registry_path);
if (!file) {
LOG_INFO("No existing app registry found");
return;
}
try {
json j;
file >> j;
if (j.contains("apps") && j["apps"].is_array()) {
for (const auto& app_json : j["apps"]) {
InstalledApp app;
app.package_id = app_json.value("package_id", "");
app.name = app_json.value("name", "");
app.version_name = app_json.value("version_name", "");
app.version_code = app_json.value("version_code", 0);
app.install_path = app_json.value("install_path", "");
app.package_size = app_json.value("package_size", 0);
app.data_size = app_json.value("data_size", 0);
app.is_system_app = app_json.value("is_system_app", false);
app.entry_point = app_json.value("entry_point", "main.rml");
app.icon_path = app_json.value("icon_path", "");
app.developer_name = app_json.value("developer_name", "");
if (app_json.contains("permissions") && app_json["permissions"].is_array()) {
for (const auto& perm : app_json["permissions"]) {
app.permissions.push_back(perm.get<std::string>());
}
}
// Parse timestamps
if (app_json.contains("installed_at")) {
auto ts = app_json["installed_at"].get<int64_t>();
app.installed_at = std::chrono::system_clock::time_point(
std::chrono::seconds(ts));
}
if (app_json.contains("updated_at")) {
auto ts = app_json["updated_at"].get<int64_t>();
app.updated_at = std::chrono::system_clock::time_point(
std::chrono::seconds(ts));
}
if (!app.package_id.empty()) {
m_installed_apps[app.package_id] = app;
}
}
}
LOG_INFO("Loaded %zu installed apps", m_installed_apps.size());
} catch (const std::exception& e) {
LOG_ERROR("Error loading app registry: %s", e.what());
}
}
void AppManager::SaveInstalledApps() {
std::string registry_path = m_data_root + "/config/apps.json";
json j;
j["version"] = 1;
j["apps"] = json::array();
for (const auto& [id, app] : m_installed_apps) {
json app_json;
app_json["package_id"] = app.package_id;
app_json["name"] = app.name;
app_json["version_name"] = app.version_name;
app_json["version_code"] = app.version_code;
app_json["install_path"] = app.install_path;
app_json["permissions"] = app.permissions;
app_json["package_size"] = app.package_size;
app_json["data_size"] = app.data_size;
app_json["is_system_app"] = app.is_system_app;
app_json["entry_point"] = app.entry_point;
app_json["icon_path"] = app.icon_path;
app_json["developer_name"] = app.developer_name;
// Store timestamps as Unix seconds
app_json["installed_at"] = std::chrono::duration_cast<std::chrono::seconds>(
app.installed_at.time_since_epoch()).count();
app_json["updated_at"] = std::chrono::duration_cast<std::chrono::seconds>(
app.updated_at.time_since_epoch()).count();
j["apps"].push_back(app_json);
}
std::ofstream file(registry_path);
if (file) {
file << j.dump(2);
LOG_DEBUG("Saved app registry with %zu apps", m_installed_apps.size());
} else {
LOG_ERROR("Failed to save app registry");
}
}
int64_t AppManager::CalculateDirectorySize(const std::string& path) const {
int64_t size = 0;
try {
for (const auto& entry : fs::recursive_directory_iterator(path)) {
if (entry.is_regular_file()) {
size += static_cast<int64_t>(entry.file_size());
}
}
} catch (...) {
// Directory may not exist
}
return size;
}
std::string AppManager::GenerateUUID() const {
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dis(0, 15);
std::stringstream ss;
for (int i = 0; i < 32; ++i) {
if (i == 8 || i == 12 || i == 16 || i == 20) {
ss << '-';
}
ss << std::hex << dis(gen);
}
return ss.str();
}
} // namespace mosis

20
core/src/logger.cpp Normal file
View File

@@ -0,0 +1,20 @@
#include <mosis/util/logger.h>
#include <iostream>
#ifdef MOSIS_PLATFORM_ANDROID
#include <android/log.h>
#define LOG_TAG "MosisOS"
void Logger::Log(const std::string &message)
{
__android_log_print(ANDROID_LOG_INFO, LOG_TAG, "%s", message.c_str());
}
#else
// Desktop/other platforms - use stdout
void Logger::Log(const std::string &message)
{
std::cout << message << std::endl;
}
#endif

View File

@@ -0,0 +1,188 @@
#include "audit_log.h"
#include <algorithm>
namespace mosis {
//=============================================================================
// CONSTRUCTOR
//=============================================================================
AuditLog::AuditLog(size_t max_entries)
: m_max_entries(max_entries)
{
m_entries.resize(max_entries);
}
//=============================================================================
// LOGGING
//=============================================================================
void AuditLog::Log(AuditEvent event, const std::string& app_id,
const std::string& details, bool success) {
std::lock_guard<std::mutex> lock(m_mutex);
AuditEntry entry{
.timestamp = std::chrono::system_clock::now(),
.event = event,
.app_id = app_id,
.details = details,
.success = success
};
m_entries[m_write_index] = std::move(entry);
m_write_index = (m_write_index + 1) % m_max_entries;
m_total_logged++;
if (m_total_logged > m_max_entries) {
m_wrapped = true;
}
}
//=============================================================================
// QUERIES
//=============================================================================
std::vector<AuditEntry> AuditLog::GetEntries(size_t count) const {
std::lock_guard<std::mutex> lock(m_mutex);
std::vector<AuditEntry> result;
size_t stored = GetStoredEntries();
count = std::min(count, stored);
result.reserve(count);
// Read from most recent backwards
for (size_t i = 0; i < count; i++) {
size_t idx = (m_write_index + m_max_entries - 1 - i) % m_max_entries;
result.push_back(m_entries[idx]);
}
return result;
}
std::vector<AuditEntry> AuditLog::GetEntriesForApp(const std::string& app_id,
size_t count) const {
std::lock_guard<std::mutex> lock(m_mutex);
std::vector<AuditEntry> result;
result.reserve(count);
size_t stored = GetStoredEntries();
for (size_t i = 0; i < stored && result.size() < count; i++) {
size_t idx = (m_write_index + m_max_entries - 1 - i) % m_max_entries;
if (m_entries[idx].app_id == app_id) {
result.push_back(m_entries[idx]);
}
}
return result;
}
std::vector<AuditEntry> AuditLog::GetEntriesByEvent(AuditEvent event,
size_t count) const {
std::lock_guard<std::mutex> lock(m_mutex);
std::vector<AuditEntry> result;
result.reserve(count);
size_t stored = GetStoredEntries();
for (size_t i = 0; i < stored && result.size() < count; i++) {
size_t idx = (m_write_index + m_max_entries - 1 - i) % m_max_entries;
if (m_entries[idx].event == event) {
result.push_back(m_entries[idx]);
}
}
return result;
}
//=============================================================================
// STATISTICS
//=============================================================================
size_t AuditLog::GetTotalEntries() const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_total_logged;
}
size_t AuditLog::GetStoredEntries() const {
// Note: caller should hold lock
if (m_wrapped) {
return m_max_entries;
}
return m_write_index;
}
size_t AuditLog::CountEvents(AuditEvent event, const std::string& app_id) const {
std::lock_guard<std::mutex> lock(m_mutex);
size_t count = 0;
size_t stored = GetStoredEntries();
for (size_t i = 0; i < stored; i++) {
const auto& entry = m_entries[i];
if (entry.event == event) {
if (app_id.empty() || entry.app_id == app_id) {
count++;
}
}
}
return count;
}
//=============================================================================
// CLEAR
//=============================================================================
void AuditLog::Clear() {
std::lock_guard<std::mutex> lock(m_mutex);
m_write_index = 0;
m_total_logged = 0;
m_wrapped = false;
// Clear all entries
for (auto& entry : m_entries) {
entry = AuditEntry{};
}
}
//=============================================================================
// UTILITIES
//=============================================================================
const char* AuditLog::EventToString(AuditEvent event) {
switch (event) {
case AuditEvent::AppStart: return "AppStart";
case AuditEvent::AppStop: return "AppStop";
case AuditEvent::PermissionCheck: return "PermissionCheck";
case AuditEvent::PermissionGranted: return "PermissionGranted";
case AuditEvent::PermissionDenied: return "PermissionDenied";
case AuditEvent::NetworkRequest: return "NetworkRequest";
case AuditEvent::NetworkBlocked: return "NetworkBlocked";
case AuditEvent::FileAccess: return "FileAccess";
case AuditEvent::FileBlocked: return "FileBlocked";
case AuditEvent::DatabaseAccess: return "DatabaseAccess";
case AuditEvent::CameraAccess: return "CameraAccess";
case AuditEvent::MicrophoneAccess: return "MicrophoneAccess";
case AuditEvent::LocationAccess: return "LocationAccess";
case AuditEvent::SandboxViolation: return "SandboxViolation";
case AuditEvent::ResourceLimitHit: return "ResourceLimitHit";
case AuditEvent::RateLimitHit: return "RateLimitHit";
case AuditEvent::Custom: return "Custom";
default: return "Unknown";
}
}
//=============================================================================
// GLOBAL INSTANCE
//=============================================================================
AuditLog& GetAuditLog() {
static AuditLog instance(10000);
return instance;
}
} // namespace mosis

View File

@@ -0,0 +1,393 @@
#include "crypto_api.h"
#include <lua.hpp>
#include <sstream>
#include <iomanip>
#include <cstring>
#ifdef _WIN32
#include <windows.h>
#include <bcrypt.h>
#pragma comment(lib, "bcrypt.lib")
#endif
namespace mosis {
//=============================================================================
// SECURE RANDOM
//=============================================================================
SecureRandom::SecureRandom()
: m_gen(m_rd()) {
}
std::string SecureRandom::GetBytes(size_t count) {
std::lock_guard<std::mutex> lock(m_mutex);
std::string result(count, '\0');
for (size_t i = 0; i < count; i++) {
result[i] = static_cast<char>(m_gen() & 0xFF);
}
return result;
}
int64_t SecureRandom::GetInt(int64_t min, int64_t max) {
std::lock_guard<std::mutex> lock(m_mutex);
std::uniform_int_distribution<int64_t> dist(min, max);
return dist(m_gen);
}
double SecureRandom::GetDouble() {
std::lock_guard<std::mutex> lock(m_mutex);
std::uniform_real_distribution<double> dist(0.0, 1.0);
return dist(m_gen);
}
//=============================================================================
// HASHING (Windows BCrypt)
//=============================================================================
#ifdef _WIN32
static std::string BytesToHex(const unsigned char* data, size_t len) {
std::ostringstream oss;
oss << std::hex << std::setfill('0');
for (size_t i = 0; i < len; i++) {
oss << std::setw(2) << static_cast<int>(data[i]);
}
return oss.str();
}
static LPCWSTR GetBCryptAlgorithm(HashAlgorithm algo) {
switch (algo) {
case HashAlgorithm::SHA256: return BCRYPT_SHA256_ALGORITHM;
case HashAlgorithm::SHA512: return BCRYPT_SHA512_ALGORITHM;
case HashAlgorithm::SHA1: return BCRYPT_SHA1_ALGORITHM;
case HashAlgorithm::MD5: return BCRYPT_MD5_ALGORITHM;
default: return BCRYPT_SHA256_ALGORITHM;
}
}
std::string ComputeHash(HashAlgorithm algo, const std::string& data) {
BCRYPT_ALG_HANDLE hAlg = nullptr;
BCRYPT_HASH_HANDLE hHash = nullptr;
NTSTATUS status;
std::string result;
status = BCryptOpenAlgorithmProvider(&hAlg, GetBCryptAlgorithm(algo), nullptr, 0);
if (!BCRYPT_SUCCESS(status)) {
return "";
}
DWORD hashLength = 0;
DWORD resultLength = 0;
status = BCryptGetProperty(hAlg, BCRYPT_HASH_LENGTH, (PUCHAR)&hashLength,
sizeof(hashLength), &resultLength, 0);
if (!BCRYPT_SUCCESS(status)) {
BCryptCloseAlgorithmProvider(hAlg, 0);
return "";
}
std::vector<unsigned char> hashBuffer(hashLength);
status = BCryptCreateHash(hAlg, &hHash, nullptr, 0, nullptr, 0, 0);
if (!BCRYPT_SUCCESS(status)) {
BCryptCloseAlgorithmProvider(hAlg, 0);
return "";
}
status = BCryptHashData(hHash, (PUCHAR)data.data(), static_cast<ULONG>(data.size()), 0);
if (!BCRYPT_SUCCESS(status)) {
BCryptDestroyHash(hHash);
BCryptCloseAlgorithmProvider(hAlg, 0);
return "";
}
status = BCryptFinishHash(hHash, hashBuffer.data(), hashLength, 0);
if (BCRYPT_SUCCESS(status)) {
result = BytesToHex(hashBuffer.data(), hashLength);
}
BCryptDestroyHash(hHash);
BCryptCloseAlgorithmProvider(hAlg, 0);
return result;
}
std::string ComputeHMAC(HashAlgorithm algo, const std::string& key, const std::string& data) {
BCRYPT_ALG_HANDLE hAlg = nullptr;
BCRYPT_HASH_HANDLE hHash = nullptr;
NTSTATUS status;
std::string result;
status = BCryptOpenAlgorithmProvider(&hAlg, GetBCryptAlgorithm(algo), nullptr,
BCRYPT_ALG_HANDLE_HMAC_FLAG);
if (!BCRYPT_SUCCESS(status)) {
return "";
}
DWORD hashLength = 0;
DWORD resultLength = 0;
status = BCryptGetProperty(hAlg, BCRYPT_HASH_LENGTH, (PUCHAR)&hashLength,
sizeof(hashLength), &resultLength, 0);
if (!BCRYPT_SUCCESS(status)) {
BCryptCloseAlgorithmProvider(hAlg, 0);
return "";
}
std::vector<unsigned char> hashBuffer(hashLength);
status = BCryptCreateHash(hAlg, &hHash, nullptr, 0,
(PUCHAR)key.data(), static_cast<ULONG>(key.size()), 0);
if (!BCRYPT_SUCCESS(status)) {
BCryptCloseAlgorithmProvider(hAlg, 0);
return "";
}
status = BCryptHashData(hHash, (PUCHAR)data.data(), static_cast<ULONG>(data.size()), 0);
if (!BCRYPT_SUCCESS(status)) {
BCryptDestroyHash(hHash);
BCryptCloseAlgorithmProvider(hAlg, 0);
return "";
}
status = BCryptFinishHash(hHash, hashBuffer.data(), hashLength, 0);
if (BCRYPT_SUCCESS(status)) {
result = BytesToHex(hashBuffer.data(), hashLength);
}
BCryptDestroyHash(hHash);
BCryptCloseAlgorithmProvider(hAlg, 0);
return result;
}
#else
// Stub implementations for non-Windows (would need OpenSSL or similar)
std::string ComputeHash(HashAlgorithm algo, const std::string& data) {
(void)algo;
(void)data;
return "";
}
std::string ComputeHMAC(HashAlgorithm algo, const std::string& key, const std::string& data) {
(void)algo;
(void)key;
(void)data;
return "";
}
#endif
//=============================================================================
// LUA CRYPTO API
//=============================================================================
static const char* CRYPTO_RNG_KEY = "__mosis_crypto_rng";
static SecureRandom* GetRng(lua_State* L) {
lua_getfield(L, LUA_REGISTRYINDEX, CRYPTO_RNG_KEY);
if (lua_islightuserdata(L, -1)) {
SecureRandom* rng = static_cast<SecureRandom*>(lua_touserdata(L, -1));
lua_pop(L, 1);
return rng;
}
lua_pop(L, 1);
// Create a default RNG if none registered
static SecureRandom default_rng;
return &default_rng;
}
// crypto.randomBytes(n) -> string
static int lua_crypto_randomBytes(lua_State* L) {
lua_Integer n = luaL_checkinteger(L, 1);
if (n < 0) {
return luaL_error(L, "crypto.randomBytes: count must be non-negative");
}
if (n > 1024) {
return luaL_error(L, "crypto.randomBytes: count must not exceed 1024");
}
SecureRandom* rng = GetRng(L);
std::string bytes = rng->GetBytes(static_cast<size_t>(n));
lua_pushlstring(L, bytes.data(), bytes.size());
return 1;
}
static HashAlgorithm ParseAlgorithm(const char* name) {
if (strcmp(name, "sha256") == 0) return HashAlgorithm::SHA256;
if (strcmp(name, "sha512") == 0) return HashAlgorithm::SHA512;
if (strcmp(name, "sha1") == 0) return HashAlgorithm::SHA1;
if (strcmp(name, "md5") == 0) return HashAlgorithm::MD5;
return HashAlgorithm::SHA256; // Default
}
// crypto.hash(algorithm, data) -> string
static int lua_crypto_hash(lua_State* L) {
const char* algo_name = luaL_checkstring(L, 1);
size_t data_len;
const char* data = luaL_checklstring(L, 2, &data_len);
// Limit input size
if (data_len > 10 * 1024 * 1024) {
return luaL_error(L, "crypto.hash: input too large (max 10MB)");
}
HashAlgorithm algo = ParseAlgorithm(algo_name);
std::string result = ComputeHash(algo, std::string(data, data_len));
if (result.empty()) {
return luaL_error(L, "crypto.hash: failed to compute hash");
}
lua_pushstring(L, result.c_str());
return 1;
}
// crypto.hmac(algorithm, key, data) -> string
static int lua_crypto_hmac(lua_State* L) {
const char* algo_name = luaL_checkstring(L, 1);
size_t key_len;
const char* key = luaL_checklstring(L, 2, &key_len);
size_t data_len;
const char* data = luaL_checklstring(L, 3, &data_len);
// Limit input sizes
if (key_len > 1024) {
return luaL_error(L, "crypto.hmac: key too large (max 1KB)");
}
if (data_len > 10 * 1024 * 1024) {
return luaL_error(L, "crypto.hmac: data too large (max 10MB)");
}
HashAlgorithm algo = ParseAlgorithm(algo_name);
std::string result = ComputeHMAC(algo, std::string(key, key_len),
std::string(data, data_len));
if (result.empty()) {
return luaL_error(L, "crypto.hmac: failed to compute HMAC");
}
lua_pushstring(L, result.c_str());
return 1;
}
// Helper to set a global in the real _G (bypassing any proxy)
static void SetGlobalInRealG(lua_State* L, const char* name) {
// Stack: value to set as global
// Get _G (might be a proxy)
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS);
// Check if it has a metatable with __index (proxy pattern)
if (lua_getmetatable(L, -1)) {
lua_getfield(L, -1, "__index");
if (lua_istable(L, -1)) {
// Found real _G through proxy's __index
// Stack: value, proxy, mt, real_G
lua_pushvalue(L, -4); // Copy value
lua_setfield(L, -2, name); // real_G[name] = value
lua_pop(L, 4); // pop real_G, mt, proxy, original value
return;
}
lua_pop(L, 2); // pop __index, metatable
}
// No proxy, set directly in _G
// Stack: value, _G
lua_pushvalue(L, -2); // Copy value
lua_setfield(L, -2, name); // _G[name] = value
lua_pop(L, 2); // pop _G, original value
}
void RegisterCryptoAPI(lua_State* L) {
// Create crypto table
lua_newtable(L);
lua_pushcfunction(L, lua_crypto_randomBytes);
lua_setfield(L, -2, "randomBytes");
lua_pushcfunction(L, lua_crypto_hash);
lua_setfield(L, -2, "hash");
lua_pushcfunction(L, lua_crypto_hmac);
lua_setfield(L, -2, "hmac");
// Set as global (bypassing proxy)
SetGlobalInRealG(L, "crypto");
}
//=============================================================================
// SECURE MATH.RANDOM
//=============================================================================
static const char* MATH_RNG_KEY = "__mosis_math_rng";
// math.random([m [, n]]) - secure version
static int lua_secure_random(lua_State* L) {
lua_getfield(L, LUA_REGISTRYINDEX, MATH_RNG_KEY);
if (!lua_islightuserdata(L, -1)) {
lua_pop(L, 1);
return luaL_error(L, "math.random: RNG not initialized");
}
SecureRandom* rng = static_cast<SecureRandom*>(lua_touserdata(L, -1));
lua_pop(L, 1);
int nargs = lua_gettop(L);
if (nargs == 0) {
// Return double in [0.0, 1.0)
lua_pushnumber(L, rng->GetDouble());
return 1;
} else if (nargs == 1) {
// Return integer in [1, n]
lua_Integer n = luaL_checkinteger(L, 1);
if (n < 1) {
return luaL_error(L, "math.random: interval is empty");
}
lua_pushinteger(L, rng->GetInt(1, n));
return 1;
} else {
// Return integer in [m, n]
lua_Integer m = luaL_checkinteger(L, 1);
lua_Integer n = luaL_checkinteger(L, 2);
if (m > n) {
return luaL_error(L, "math.random: interval is empty");
}
lua_pushinteger(L, rng->GetInt(m, n));
return 1;
}
}
void RegisterSecureMathRandom(lua_State* L, SecureRandom* rng) {
// Store RNG in registry
lua_pushlightuserdata(L, rng);
lua_setfield(L, LUA_REGISTRYINDEX, MATH_RNG_KEY);
// Also store for crypto API
lua_pushlightuserdata(L, rng);
lua_setfield(L, LUA_REGISTRYINDEX, CRYPTO_RNG_KEY);
// Get the math table
lua_getglobal(L, "math");
if (!lua_istable(L, -1)) {
lua_pop(L, 1);
return;
}
// Replace math.random with secure version
lua_pushcfunction(L, lua_secure_random);
lua_setfield(L, -2, "random");
// Remove math.randomseed
lua_pushnil(L);
lua_setfield(L, -2, "randomseed");
lua_pop(L, 1); // Pop math table
}
} // namespace mosis

View File

@@ -0,0 +1,598 @@
#include "database_manager.h"
#include <sqlite3.h>
#include <lua.hpp>
#include <filesystem>
#include <algorithm>
#include <cctype>
namespace fs = std::filesystem;
namespace mosis {
// Helper to set a global in the real _G (bypassing any proxy)
static void SetGlobalInRealG(lua_State* L, const char* name) {
// Stack: value to set as global
// Get _G (might be a proxy)
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS);
// Check if it has a metatable with __index (proxy pattern)
if (lua_getmetatable(L, -1)) {
lua_getfield(L, -1, "__index");
if (lua_istable(L, -1)) {
// Found real _G through proxy's __index
// Stack: value, proxy, mt, real_G
lua_pushvalue(L, -4); // Copy value
lua_setfield(L, -2, name); // real_G[name] = value
lua_pop(L, 4); // pop real_G, mt, proxy, original value
return;
}
lua_pop(L, 2); // pop __index, metatable
}
// No proxy, set directly in _G
// Stack: value, _G
lua_pushvalue(L, -2); // Copy value
lua_setfield(L, -2, name); // _G[name] = value
lua_pop(L, 2); // pop _G, original value
}
// ============================================================================
// DatabaseManager
// ============================================================================
DatabaseManager::DatabaseManager(const std::string& app_id,
const std::string& app_root,
const DatabaseLimits& limits)
: m_app_id(app_id)
, m_app_root(app_root)
, m_limits(limits) {
}
DatabaseManager::~DatabaseManager() {
CloseAll();
}
bool DatabaseManager::ValidateName(const std::string& name, std::string& error) {
if (name.empty()) {
error = "Database name cannot be empty";
return false;
}
if (name.length() > 64) {
error = "Database name too long (max 64 characters)";
return false;
}
// Check for path traversal
if (name.find("..") != std::string::npos) {
error = "Database name contains invalid path traversal";
return false;
}
// Check for path separators
if (name.find('/') != std::string::npos || name.find('\\') != std::string::npos) {
error = "Database name cannot contain path separators";
return false;
}
// Only allow alphanumeric, underscore, hyphen
for (char c : name) {
if (!std::isalnum(static_cast<unsigned char>(c)) && c != '_' && c != '-') {
error = "Database name contains invalid characters (only alphanumeric, underscore, hyphen allowed)";
return false;
}
}
return true;
}
std::string DatabaseManager::ResolvePath(const std::string& name) {
fs::path db_dir = fs::path(m_app_root) / "db";
return (db_dir / (name + ".db")).string();
}
std::shared_ptr<DatabaseHandle> DatabaseManager::Open(const std::string& name, std::string& error) {
// Validate name
if (!ValidateName(name, error)) {
return nullptr;
}
// Check if already open
auto it = m_databases.find(name);
if (it != m_databases.end() && it->second->IsOpen()) {
return it->second;
}
// Check max databases limit
if (m_databases.size() >= static_cast<size_t>(m_limits.max_databases_per_app)) {
error = "Maximum number of open databases reached";
return nullptr;
}
// Resolve path and ensure directory exists
std::string db_path = ResolvePath(name);
fs::path parent = fs::path(db_path).parent_path();
std::error_code ec;
fs::create_directories(parent, ec);
if (ec) {
error = "Failed to create database directory: " + ec.message();
return nullptr;
}
// Open SQLite database
sqlite3* db = nullptr;
int rc = sqlite3_open(db_path.c_str(), &db);
if (rc != SQLITE_OK) {
error = "Failed to open database: " + std::string(sqlite3_errmsg(db));
sqlite3_close(db);
return nullptr;
}
// Create handle
auto handle = std::make_shared<DatabaseHandle>(db, db_path, m_limits);
m_databases[name] = handle;
return handle;
}
void DatabaseManager::CloseAll() {
for (auto& [name, handle] : m_databases) {
if (handle) {
handle->Close();
}
}
m_databases.clear();
}
size_t DatabaseManager::GetOpenDatabaseCount() const {
size_t count = 0;
for (const auto& [name, handle] : m_databases) {
if (handle && handle->IsOpen()) {
count++;
}
}
return count;
}
// ============================================================================
// DatabaseHandle
// ============================================================================
DatabaseHandle::DatabaseHandle(sqlite3* db, const std::string& path, const DatabaseLimits& limits)
: m_db(db)
, m_path(path)
, m_limits(limits) {
if (m_db) {
// Set up authorizer
sqlite3_set_authorizer(m_db, Authorizer, this);
// Set busy timeout
sqlite3_busy_timeout(m_db, m_limits.max_query_time_ms);
}
}
DatabaseHandle::~DatabaseHandle() {
Close();
}
void DatabaseHandle::Close() {
if (m_db) {
sqlite3_close(m_db);
m_db = nullptr;
}
}
int DatabaseHandle::Authorizer(void* user_data, int action, const char* arg1,
const char* arg2, const char* arg3, const char* arg4) {
(void)user_data;
(void)arg3;
(void)arg4;
switch (action) {
case SQLITE_ATTACH:
case SQLITE_DETACH:
// Block attaching/detaching databases
return SQLITE_DENY;
case SQLITE_PRAGMA: {
// Allow safe pragmas only
if (arg1) {
std::string pragma(arg1);
// Convert to lowercase for comparison
std::transform(pragma.begin(), pragma.end(), pragma.begin(),
[](unsigned char c) { return std::tolower(c); });
// Whitelist of safe pragmas
if (pragma == "table_info" ||
pragma == "index_list" ||
pragma == "index_info" ||
pragma == "foreign_keys" ||
pragma == "foreign_key_list" ||
pragma == "database_list" ||
pragma == "table_list" ||
pragma == "integrity_check" ||
pragma == "quick_check") {
return SQLITE_OK;
}
// Block all other pragmas
return SQLITE_DENY;
}
return SQLITE_DENY;
}
case SQLITE_FUNCTION: {
// Block dangerous functions
if (arg2) {
std::string func(arg2);
std::transform(func.begin(), func.end(), func.begin(),
[](unsigned char c) { return std::tolower(c); });
if (func == "load_extension") {
return SQLITE_DENY;
}
}
return SQLITE_OK;
}
default:
return SQLITE_OK;
}
}
bool DatabaseHandle::BindParameters(void* stmt_ptr, const std::vector<SqlValue>& params, std::string& error) {
sqlite3_stmt* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
for (size_t i = 0; i < params.size(); i++) {
int idx = static_cast<int>(i + 1); // SQLite parameters are 1-indexed
int rc = SQLITE_OK;
std::visit([&](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, std::nullptr_t>) {
rc = sqlite3_bind_null(stmt, idx);
} else if constexpr (std::is_same_v<T, int64_t>) {
rc = sqlite3_bind_int64(stmt, idx, arg);
} else if constexpr (std::is_same_v<T, double>) {
rc = sqlite3_bind_double(stmt, idx, arg);
} else if constexpr (std::is_same_v<T, std::string>) {
rc = sqlite3_bind_text(stmt, idx, arg.c_str(), static_cast<int>(arg.size()), SQLITE_TRANSIENT);
} else if constexpr (std::is_same_v<T, std::vector<uint8_t>>) {
rc = sqlite3_bind_blob(stmt, idx, arg.data(), static_cast<int>(arg.size()), SQLITE_TRANSIENT);
}
}, params[i]);
if (rc != SQLITE_OK) {
error = "Failed to bind parameter " + std::to_string(i) + ": " + sqlite3_errmsg(m_db);
return false;
}
}
return true;
}
bool DatabaseHandle::Execute(const std::string& sql, const std::vector<SqlValue>& params, std::string& error) {
if (!m_db) {
error = "Database not open";
return false;
}
sqlite3_stmt* stmt = nullptr;
int rc = sqlite3_prepare_v2(m_db, sql.c_str(), static_cast<int>(sql.size()), &stmt, nullptr);
if (rc != SQLITE_OK) {
error = "SQL prepare error: " + std::string(sqlite3_errmsg(m_db));
return false;
}
if (!BindParameters(stmt, params, error)) {
sqlite3_finalize(stmt);
return false;
}
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE && rc != SQLITE_ROW) {
error = "SQL execution error: " + std::string(sqlite3_errmsg(m_db));
return false;
}
return true;
}
std::optional<SqlResult> DatabaseHandle::Query(const std::string& sql, const std::vector<SqlValue>& params,
std::string& error) {
if (!m_db) {
error = "Database not open";
return std::nullopt;
}
sqlite3_stmt* stmt = nullptr;
int rc = sqlite3_prepare_v2(m_db, sql.c_str(), static_cast<int>(sql.size()), &stmt, nullptr);
if (rc != SQLITE_OK) {
error = "SQL prepare error: " + std::string(sqlite3_errmsg(m_db));
return std::nullopt;
}
if (!BindParameters(stmt, params, error)) {
sqlite3_finalize(stmt);
return std::nullopt;
}
SqlResult result;
int row_count = 0;
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
if (row_count >= m_limits.max_result_rows) {
error = "Result row limit exceeded";
sqlite3_finalize(stmt);
return std::nullopt;
}
int col_count = sqlite3_column_count(stmt);
SqlRow row;
row.reserve(col_count);
for (int i = 0; i < col_count; i++) {
int type = sqlite3_column_type(stmt, i);
switch (type) {
case SQLITE_NULL:
row.push_back(nullptr);
break;
case SQLITE_INTEGER:
row.push_back(sqlite3_column_int64(stmt, i));
break;
case SQLITE_FLOAT:
row.push_back(sqlite3_column_double(stmt, i));
break;
case SQLITE_TEXT: {
const char* text = reinterpret_cast<const char*>(sqlite3_column_text(stmt, i));
int len = sqlite3_column_bytes(stmt, i);
row.push_back(std::string(text, len));
break;
}
case SQLITE_BLOB: {
const uint8_t* data = static_cast<const uint8_t*>(sqlite3_column_blob(stmt, i));
int len = sqlite3_column_bytes(stmt, i);
row.push_back(std::vector<uint8_t>(data, data + len));
break;
}
}
}
result.push_back(std::move(row));
row_count++;
}
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) {
error = "SQL query error: " + std::string(sqlite3_errmsg(m_db));
return std::nullopt;
}
return result;
}
int64_t DatabaseHandle::GetLastInsertRowId() const {
if (!m_db) return 0;
return sqlite3_last_insert_rowid(m_db);
}
int DatabaseHandle::GetChanges() const {
if (!m_db) return 0;
return sqlite3_changes(m_db);
}
// ============================================================================
// Lua API
// ============================================================================
struct LuaDatabaseHandle {
std::shared_ptr<DatabaseHandle> handle;
};
static int Lua_DatabaseHandle_Execute(lua_State* L) {
LuaDatabaseHandle* lh = static_cast<LuaDatabaseHandle*>(luaL_checkudata(L, 1, "DatabaseHandle"));
if (!lh->handle || !lh->handle->IsOpen()) {
lua_pushboolean(L, 0);
lua_pushstring(L, "Database not open");
return 2;
}
const char* sql = luaL_checkstring(L, 2);
// Get parameters from optional table
std::vector<SqlValue> params;
if (lua_gettop(L) >= 3 && lua_istable(L, 3)) {
lua_pushnil(L);
while (lua_next(L, 3) != 0) {
if (lua_isnil(L, -1)) {
params.push_back(nullptr);
} else if (lua_isinteger(L, -1)) {
params.push_back(static_cast<int64_t>(lua_tointeger(L, -1)));
} else if (lua_isnumber(L, -1)) {
params.push_back(lua_tonumber(L, -1));
} else if (lua_isstring(L, -1)) {
size_t len;
const char* str = lua_tolstring(L, -1, &len);
params.push_back(std::string(str, len));
} else if (lua_isboolean(L, -1)) {
params.push_back(static_cast<int64_t>(lua_toboolean(L, -1)));
}
lua_pop(L, 1);
}
}
std::string error;
if (lh->handle->Execute(sql, params, error)) {
lua_pushboolean(L, 1);
return 1;
} else {
lua_pushboolean(L, 0);
lua_pushstring(L, error.c_str());
return 2;
}
}
static int Lua_DatabaseHandle_Query(lua_State* L) {
LuaDatabaseHandle* lh = static_cast<LuaDatabaseHandle*>(luaL_checkudata(L, 1, "DatabaseHandle"));
if (!lh->handle || !lh->handle->IsOpen()) {
lua_pushnil(L);
lua_pushstring(L, "Database not open");
return 2;
}
const char* sql = luaL_checkstring(L, 2);
// Get parameters from optional table
std::vector<SqlValue> params;
if (lua_gettop(L) >= 3 && lua_istable(L, 3)) {
lua_pushnil(L);
while (lua_next(L, 3) != 0) {
if (lua_isnil(L, -1)) {
params.push_back(nullptr);
} else if (lua_isinteger(L, -1)) {
params.push_back(static_cast<int64_t>(lua_tointeger(L, -1)));
} else if (lua_isnumber(L, -1)) {
params.push_back(lua_tonumber(L, -1));
} else if (lua_isstring(L, -1)) {
size_t len;
const char* str = lua_tolstring(L, -1, &len);
params.push_back(std::string(str, len));
} else if (lua_isboolean(L, -1)) {
params.push_back(static_cast<int64_t>(lua_toboolean(L, -1)));
}
lua_pop(L, 1);
}
}
std::string error;
auto result = lh->handle->Query(sql, params, error);
if (!result.has_value()) {
lua_pushnil(L);
lua_pushstring(L, error.c_str());
return 2;
}
// Create result table
lua_createtable(L, static_cast<int>(result->size()), 0);
int row_idx = 1;
for (const auto& row : *result) {
lua_createtable(L, static_cast<int>(row.size()), 0);
int col_idx = 1;
for (const auto& val : row) {
std::visit([L](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, std::nullptr_t>) {
lua_pushnil(L);
} else if constexpr (std::is_same_v<T, int64_t>) {
lua_pushinteger(L, arg);
} else if constexpr (std::is_same_v<T, double>) {
lua_pushnumber(L, arg);
} else if constexpr (std::is_same_v<T, std::string>) {
lua_pushlstring(L, arg.c_str(), arg.size());
} else if constexpr (std::is_same_v<T, std::vector<uint8_t>>) {
lua_pushlstring(L, reinterpret_cast<const char*>(arg.data()), arg.size());
}
}, val);
lua_rawseti(L, -2, col_idx++);
}
lua_rawseti(L, -2, row_idx++);
}
return 1;
}
static int Lua_DatabaseHandle_LastInsertId(lua_State* L) {
LuaDatabaseHandle* lh = static_cast<LuaDatabaseHandle*>(luaL_checkudata(L, 1, "DatabaseHandle"));
if (!lh->handle) {
lua_pushinteger(L, 0);
return 1;
}
lua_pushinteger(L, lh->handle->GetLastInsertRowId());
return 1;
}
static int Lua_DatabaseHandle_Changes(lua_State* L) {
LuaDatabaseHandle* lh = static_cast<LuaDatabaseHandle*>(luaL_checkudata(L, 1, "DatabaseHandle"));
if (!lh->handle) {
lua_pushinteger(L, 0);
return 1;
}
lua_pushinteger(L, lh->handle->GetChanges());
return 1;
}
static int Lua_DatabaseHandle_Close(lua_State* L) {
LuaDatabaseHandle* lh = static_cast<LuaDatabaseHandle*>(luaL_checkudata(L, 1, "DatabaseHandle"));
if (lh->handle) {
lh->handle->Close();
}
return 0;
}
static int Lua_DatabaseHandle_GC(lua_State* L) {
LuaDatabaseHandle* lh = static_cast<LuaDatabaseHandle*>(luaL_checkudata(L, 1, "DatabaseHandle"));
lh->~LuaDatabaseHandle();
return 0;
}
static const luaL_Reg DatabaseHandle_methods[] = {
{"execute", Lua_DatabaseHandle_Execute},
{"query", Lua_DatabaseHandle_Query},
{"lastInsertId", Lua_DatabaseHandle_LastInsertId},
{"changes", Lua_DatabaseHandle_Changes},
{"close", Lua_DatabaseHandle_Close},
{nullptr, nullptr}
};
static int Lua_Database_Open(lua_State* L) {
DatabaseManager* manager = static_cast<DatabaseManager*>(lua_touserdata(L, lua_upvalueindex(1)));
const char* name = luaL_checkstring(L, 1);
std::string error;
auto handle = manager->Open(name, error);
if (!handle) {
lua_pushnil(L);
lua_pushstring(L, error.c_str());
return 2;
}
// Create userdata
LuaDatabaseHandle* lh = static_cast<LuaDatabaseHandle*>(lua_newuserdata(L, sizeof(LuaDatabaseHandle)));
new (lh) LuaDatabaseHandle{handle};
luaL_getmetatable(L, "DatabaseHandle");
lua_setmetatable(L, -2);
return 1;
}
void RegisterDatabaseAPI(lua_State* L, DatabaseManager* manager) {
// Create DatabaseHandle metatable
luaL_newmetatable(L, "DatabaseHandle");
lua_pushvalue(L, -1);
lua_setfield(L, -2, "__index");
lua_pushcfunction(L, Lua_DatabaseHandle_GC);
lua_setfield(L, -2, "__gc");
luaL_setfuncs(L, DatabaseHandle_methods, 0);
lua_pop(L, 1);
// Create database table
lua_newtable(L);
// database.open
lua_pushlightuserdata(L, manager);
lua_pushcclosure(L, Lua_Database_Open, 1);
lua_setfield(L, -2, "open");
// Set as global
SetGlobalInRealG(L, "database");
}
} // namespace mosis

View File

@@ -0,0 +1,388 @@
#include "http_validator.h"
#include <algorithm>
#include <cctype>
#include <regex>
#include <array>
namespace mosis {
HttpValidator::HttpValidator()
: m_domain_restrictions_enabled(false)
{
}
void HttpValidator::SetAllowedDomains(const std::vector<std::string>& domains) {
m_allowed_domains = domains;
m_domain_restrictions_enabled = !domains.empty();
}
void HttpValidator::ClearDomainRestrictions() {
m_allowed_domains.clear();
m_domain_restrictions_enabled = false;
}
std::optional<ParsedUrl> HttpValidator::Validate(const std::string& url, std::string& error) {
// Parse URL
auto parsed = ParseUrl(url);
if (!parsed) {
error = "Invalid URL format";
return std::nullopt;
}
// Must be HTTPS or WSS
if (parsed->scheme != "https" && parsed->scheme != "wss") {
error = "HTTPS or WSS required, got: " + parsed->scheme;
return std::nullopt;
}
// Check for localhost names
if (IsLocalhostName(parsed->host)) {
error = "localhost blocked for security";
return std::nullopt;
}
// Check for metadata hostnames
if (IsMetadataHostname(parsed->host)) {
error = "Cloud metadata hostname blocked for security";
return std::nullopt;
}
// Check if it's an IP address and validate
if (parsed->is_ip_address) {
if (IsBlockedIP(parsed->host)) {
error = "IP address blocked: private, localhost, or metadata endpoint";
return std::nullopt;
}
}
// Check domain whitelist
if (m_domain_restrictions_enabled && !IsDomainAllowed(parsed->host)) {
error = "Domain not in allowed list: " + parsed->host;
return std::nullopt;
}
return parsed;
}
bool HttpValidator::IsIPv4Address(const std::string& host) {
// Simple IPv4 pattern: numbers and dots
if (host.empty()) return false;
int dots = 0;
int num_start = 0;
for (size_t i = 0; i <= host.length(); i++) {
if (i == host.length() || host[i] == '.') {
if (i == (size_t)num_start) return false; // Empty segment
std::string segment = host.substr(num_start, i - num_start);
// Check if segment is a valid number 0-255
if (segment.empty() || segment.length() > 3) return false;
for (char c : segment) {
if (!std::isdigit(static_cast<unsigned char>(c))) return false;
}
int val = std::stoi(segment);
if (val < 0 || val > 255) return false;
if (i < host.length()) {
dots++;
num_start = static_cast<int>(i) + 1;
}
}
}
return dots == 3;
}
bool HttpValidator::IsIPv6Address(const std::string& host) {
// IPv6 addresses in URLs are enclosed in brackets: [::1]
if (host.length() < 2) return false;
if (host.front() == '[' && host.back() == ']') {
return true; // Simplified check - bracket notation means IPv6
}
// Also check for raw IPv6 (contains colons, no dots or limited dots)
int colons = std::count(host.begin(), host.end(), ':');
int dots = std::count(host.begin(), host.end(), '.');
return colons >= 2 && dots <= 3; // IPv6 has multiple colons
}
bool HttpValidator::IsPrivateIPv4(const std::string& ip) {
// Parse IPv4 octets
std::array<int, 4> octets{};
if (sscanf(ip.c_str(), "%d.%d.%d.%d", &octets[0], &octets[1], &octets[2], &octets[3]) != 4) {
return false;
}
// 0.0.0.0 - all interfaces
if (octets[0] == 0 && octets[1] == 0 && octets[2] == 0 && octets[3] == 0) {
return true;
}
// 127.0.0.0/8 - loopback
if (octets[0] == 127) {
return true;
}
// 10.0.0.0/8 - private Class A
if (octets[0] == 10) {
return true;
}
// 172.16.0.0/12 - private Class B (172.16.0.0 - 172.31.255.255)
if (octets[0] == 172 && octets[1] >= 16 && octets[1] <= 31) {
return true;
}
// 192.168.0.0/16 - private Class C
if (octets[0] == 192 && octets[1] == 168) {
return true;
}
// 169.254.0.0/16 - link-local
if (octets[0] == 169 && octets[1] == 254) {
return true;
}
return false;
}
bool HttpValidator::IsPrivateIPv6(const std::string& ip) {
std::string addr = ip;
// Remove brackets if present
if (!addr.empty() && addr.front() == '[') addr = addr.substr(1);
if (!addr.empty() && addr.back() == ']') addr.pop_back();
// Convert to lowercase for comparison
std::transform(addr.begin(), addr.end(), addr.begin(),
[](unsigned char c) { return std::tolower(c); });
// ::1 - loopback
if (addr == "::1" || addr == "0:0:0:0:0:0:0:1") {
return true;
}
// :: - unspecified (equivalent to 0.0.0.0)
if (addr == "::" || addr == "0:0:0:0:0:0:0:0") {
return true;
}
// fc00::/7 - unique local addresses (fc00:: to fdff::)
if (addr.length() >= 2) {
char first = addr[0];
char second = addr.length() > 1 ? addr[1] : '0';
if (first == 'f' && (second == 'c' || second == 'd')) {
return true;
}
}
// fe80::/10 - link-local
if (addr.rfind("fe80:", 0) == 0 || addr.rfind("fe8", 0) == 0 ||
addr.rfind("fe9", 0) == 0 || addr.rfind("fea", 0) == 0 ||
addr.rfind("feb", 0) == 0) {
return true;
}
return false;
}
bool HttpValidator::IsLocalhostIP(const std::string& host) {
// IPv4 localhost
if (IsIPv4Address(host)) {
std::array<int, 4> octets{};
if (sscanf(host.c_str(), "%d.%d.%d.%d", &octets[0], &octets[1], &octets[2], &octets[3]) == 4) {
return octets[0] == 127;
}
}
// IPv6 localhost
std::string addr = host;
if (!addr.empty() && addr.front() == '[') addr = addr.substr(1);
if (!addr.empty() && addr.back() == ']') addr.pop_back();
std::transform(addr.begin(), addr.end(), addr.begin(),
[](unsigned char c) { return std::tolower(c); });
return addr == "::1" || addr == "0:0:0:0:0:0:0:1";
}
bool HttpValidator::IsMetadataIP(const std::string& host) {
// AWS/Azure/GCP metadata endpoint
if (host == "169.254.169.254") {
return true;
}
// GCP alternate
if (host == "metadata.google.internal") {
return true;
}
return false;
}
bool HttpValidator::IsBlockedIP(const std::string& host) {
if (IsIPv4Address(host)) {
return IsPrivateIPv4(host) || IsMetadataIP(host);
}
if (IsIPv6Address(host)) {
return IsPrivateIPv6(host);
}
return false;
}
bool HttpValidator::IsDomainAllowed(const std::string& host) {
if (!m_domain_restrictions_enabled) {
return true;
}
std::string lower_host = host;
std::transform(lower_host.begin(), lower_host.end(), lower_host.begin(),
[](unsigned char c) { return std::tolower(c); });
for (const auto& domain : m_allowed_domains) {
std::string lower_domain = domain;
std::transform(lower_domain.begin(), lower_domain.end(), lower_domain.begin(),
[](unsigned char c) { return std::tolower(c); });
// Exact match
if (lower_host == lower_domain) {
return true;
}
// Subdomain match (e.g., "api.example.com" matches "example.com")
if (lower_host.length() > lower_domain.length()) {
size_t pos = lower_host.length() - lower_domain.length();
if (lower_host[pos - 1] == '.' &&
lower_host.substr(pos) == lower_domain) {
return true;
}
}
}
return false;
}
bool HttpValidator::IsLocalhostName(const std::string& host) {
std::string lower = host;
std::transform(lower.begin(), lower.end(), lower.begin(),
[](unsigned char c) { return std::tolower(c); });
// Common localhost names
if (lower == "localhost") return true;
if (lower == "localhost.localdomain") return true;
// Ends with .localhost
if (lower.length() > 10 && lower.substr(lower.length() - 10) == ".localhost") {
return true;
}
return false;
}
bool HttpValidator::IsMetadataHostname(const std::string& host) {
std::string lower = host;
std::transform(lower.begin(), lower.end(), lower.begin(),
[](unsigned char c) { return std::tolower(c); });
// GCP metadata
if (lower == "metadata.google.internal") return true;
if (lower == "metadata") return true;
// Azure metadata
if (lower == "metadata.azure.internal") return true;
return false;
}
std::optional<ParsedUrl> HttpValidator::ParseUrl(const std::string& url) {
ParsedUrl result;
result.port = 443; // Default HTTPS port
result.is_ip_address = false;
// Find scheme
size_t scheme_end = url.find("://");
if (scheme_end == std::string::npos) {
return std::nullopt;
}
result.scheme = url.substr(0, scheme_end);
std::transform(result.scheme.begin(), result.scheme.end(), result.scheme.begin(),
[](unsigned char c) { return std::tolower(c); });
// Start of authority
size_t auth_start = scheme_end + 3;
if (auth_start >= url.length()) {
return std::nullopt;
}
// Find end of authority (path starts with /)
size_t path_start = url.find('/', auth_start);
std::string authority;
if (path_start == std::string::npos) {
authority = url.substr(auth_start);
result.path = "/";
} else {
authority = url.substr(auth_start, path_start - auth_start);
// Find query string
size_t query_start = url.find('?', path_start);
if (query_start != std::string::npos) {
result.path = url.substr(path_start, query_start - path_start);
result.query = url.substr(query_start);
} else {
result.path = url.substr(path_start);
}
}
if (authority.empty()) {
return std::nullopt;
}
// Parse authority for host and port
// Handle IPv6 addresses in brackets
if (authority[0] == '[') {
size_t bracket_end = authority.find(']');
if (bracket_end == std::string::npos) {
return std::nullopt; // Malformed IPv6
}
result.host = authority.substr(0, bracket_end + 1);
result.is_ip_address = true;
// Check for port after bracket
if (bracket_end + 1 < authority.length()) {
if (authority[bracket_end + 1] == ':') {
std::string port_str = authority.substr(bracket_end + 2);
try {
result.port = static_cast<uint16_t>(std::stoi(port_str));
} catch (...) {
return std::nullopt;
}
}
}
} else {
// Regular host or IPv4
size_t port_pos = authority.rfind(':');
if (port_pos != std::string::npos) {
result.host = authority.substr(0, port_pos);
std::string port_str = authority.substr(port_pos + 1);
try {
result.port = static_cast<uint16_t>(std::stoi(port_str));
} catch (...) {
return std::nullopt;
}
} else {
result.host = authority;
}
// Check if it's an IP address
result.is_ip_address = IsIPv4Address(result.host) || IsIPv6Address(result.host);
}
// Default port based on scheme
if ((result.scheme == "https" || result.scheme == "wss") && result.port == 0) {
result.port = 443;
} else if ((result.scheme == "http" || result.scheme == "ws") && result.port == 0) {
result.port = 80;
}
return result;
}
} // namespace mosis

View File

@@ -0,0 +1,369 @@
#include "json_api.h"
#include <lua.hpp>
#include <nlohmann/json.hpp>
#include <unordered_set>
#include <sstream>
using json = nlohmann::json;
namespace mosis {
// Registry key for storing limits
static const char* JSON_LIMITS_KEY = "__mosis_json_limits";
// Get limits from registry
static JsonLimits GetLimits(lua_State* L) {
lua_getfield(L, LUA_REGISTRYINDEX, JSON_LIMITS_KEY);
if (lua_islightuserdata(L, -1)) {
JsonLimits* limits = static_cast<JsonLimits*>(lua_touserdata(L, -1));
lua_pop(L, 1);
return *limits;
}
lua_pop(L, 1);
return JsonLimits{};
}
//=============================================================================
// JSON DECODE
//=============================================================================
// Custom exception for JSON errors (thrown instead of luaL_error to allow catching)
class JsonError : public std::runtime_error {
public:
explicit JsonError(const std::string& msg) : std::runtime_error(msg) {}
};
// Forward declaration
static void JsonToLua(lua_State* L, const json& j, const JsonLimits& limits, int depth);
static void JsonToLua(lua_State* L, const json& j, const JsonLimits& limits, int depth) {
if (depth > limits.max_depth) {
throw JsonError("maximum depth exceeded");
}
switch (j.type()) {
case json::value_t::null:
lua_pushnil(L);
break;
case json::value_t::boolean:
lua_pushboolean(L, j.get<bool>() ? 1 : 0);
break;
case json::value_t::number_integer:
case json::value_t::number_unsigned:
lua_pushinteger(L, j.get<lua_Integer>());
break;
case json::value_t::number_float:
lua_pushnumber(L, j.get<lua_Number>());
break;
case json::value_t::string: {
const std::string& s = j.get_ref<const std::string&>();
if (s.size() > limits.max_string_length) {
throw JsonError("string too large");
}
lua_pushlstring(L, s.c_str(), s.size());
break;
}
case json::value_t::array: {
if (j.size() > limits.max_array_size) {
throw JsonError("array size limit exceeded");
}
lua_createtable(L, static_cast<int>(j.size()), 0);
int i = 1;
for (const auto& elem : j) {
JsonToLua(L, elem, limits, depth + 1);
lua_rawseti(L, -2, i++);
}
break;
}
case json::value_t::object: {
if (j.size() > limits.max_object_size) {
throw JsonError("object size limit exceeded");
}
lua_createtable(L, 0, static_cast<int>(j.size()));
for (auto it = j.begin(); it != j.end(); ++it) {
if (it.key().size() > limits.max_string_length) {
throw JsonError("key too large");
}
lua_pushlstring(L, it.key().c_str(), it.key().size());
JsonToLua(L, it.value(), limits, depth + 1);
lua_rawset(L, -3);
}
break;
}
default:
lua_pushnil(L);
break;
}
}
// json.decode(str) -> table|nil, error
static int lua_json_decode(lua_State* L) {
size_t len;
const char* str = luaL_checklstring(L, 1, &len);
JsonLimits limits = GetLimits(L);
if (len > limits.max_output_size) {
lua_pushnil(L);
lua_pushstring(L, "input too large");
return 2;
}
try {
json j = json::parse(str, str + len);
JsonToLua(L, j, limits, 0);
return 1;
} catch (const JsonError& e) {
lua_pushnil(L);
lua_pushstring(L, e.what());
return 2;
} catch (const json::parse_error& e) {
lua_pushnil(L);
lua_pushstring(L, e.what());
return 2;
} catch (const std::exception& e) {
lua_pushnil(L);
lua_pushstring(L, e.what());
return 2;
}
}
//=============================================================================
// JSON ENCODE
//=============================================================================
// Forward declaration
static json LuaToJson(lua_State* L, int index, const JsonLimits& limits,
int depth, std::unordered_set<const void*>& visited,
size_t& output_size);
static json LuaToJson(lua_State* L, int index, const JsonLimits& limits,
int depth, std::unordered_set<const void*>& visited,
size_t& output_size) {
if (depth > limits.max_depth) {
throw JsonError("maximum depth exceeded");
}
if (output_size > limits.max_output_size) {
throw JsonError("output size limit exceeded");
}
int type = lua_type(L, index);
switch (type) {
case LUA_TNIL:
return nullptr;
case LUA_TBOOLEAN:
return lua_toboolean(L, index) != 0;
case LUA_TNUMBER:
if (lua_isinteger(L, index)) {
return lua_tointeger(L, index);
}
return lua_tonumber(L, index);
case LUA_TSTRING: {
size_t len;
const char* s = lua_tolstring(L, index, &len);
if (len > limits.max_string_length) {
throw JsonError("string too large");
}
output_size += len + 2; // Approximate: string + quotes
return std::string(s, len);
}
case LUA_TTABLE: {
// Check for cycles
const void* ptr = lua_topointer(L, index);
if (visited.find(ptr) != visited.end()) {
throw JsonError("circular reference detected");
}
visited.insert(ptr);
// Determine if array or object by checking keys
bool is_array = true;
size_t array_len = 0;
lua_pushnil(L);
while (lua_next(L, index) != 0) {
if (lua_type(L, -2) == LUA_TNUMBER && lua_isinteger(L, -2)) {
lua_Integer key = lua_tointeger(L, -2);
if (key >= 1) {
array_len = std::max(array_len, static_cast<size_t>(key));
} else {
is_array = false;
}
} else {
is_array = false;
}
lua_pop(L, 1);
}
// Verify array is contiguous
if (is_array && array_len > 0) {
for (size_t i = 1; i <= array_len; i++) {
lua_rawgeti(L, index, static_cast<int>(i));
if (lua_isnil(L, -1)) {
is_array = false;
}
lua_pop(L, 1);
if (!is_array) break;
}
}
if (is_array && array_len > 0) {
if (array_len > limits.max_array_size) {
throw JsonError("array size limit exceeded");
}
json arr = json::array();
for (size_t i = 1; i <= array_len; i++) {
lua_rawgeti(L, index, static_cast<int>(i));
arr.push_back(LuaToJson(L, lua_gettop(L), limits, depth + 1, visited, output_size));
lua_pop(L, 1);
}
visited.erase(ptr);
return arr;
} else {
// Object
json obj = json::object();
size_t key_count = 0;
lua_pushnil(L);
while (lua_next(L, index) != 0) {
key_count++;
if (key_count > limits.max_object_size) {
throw JsonError("object size limit exceeded");
}
// Get key as string
std::string key;
if (lua_type(L, -2) == LUA_TSTRING) {
size_t len;
const char* s = lua_tolstring(L, -2, &len);
key = std::string(s, len);
} else if (lua_type(L, -2) == LUA_TNUMBER) {
if (lua_isinteger(L, -2)) {
key = std::to_string(lua_tointeger(L, -2));
} else {
key = std::to_string(lua_tonumber(L, -2));
}
} else {
lua_pop(L, 2);
throw JsonError("unsupported key type");
}
output_size += key.size() + 3; // key + quotes + colon
obj[key] = LuaToJson(L, lua_gettop(L), limits, depth + 1, visited, output_size);
lua_pop(L, 1);
}
visited.erase(ptr);
return obj;
}
}
case LUA_TFUNCTION:
case LUA_TUSERDATA:
case LUA_TTHREAD:
case LUA_TLIGHTUSERDATA:
throw JsonError(std::string("unsupported type '") + lua_typename(L, type) + "'");
default:
return nullptr;
}
}
// json.encode(table) -> string|nil, error
static int lua_json_encode(lua_State* L) {
luaL_checktype(L, 1, LUA_TTABLE);
JsonLimits limits = GetLimits(L);
std::unordered_set<const void*> visited;
size_t output_size = 0;
try {
json j = LuaToJson(L, 1, limits, 0, visited, output_size);
std::string result = j.dump();
if (result.size() > limits.max_output_size) {
lua_pushnil(L);
lua_pushstring(L, "output size limit exceeded");
return 2;
}
lua_pushlstring(L, result.c_str(), result.size());
return 1;
} catch (const JsonError& e) {
lua_pushnil(L);
lua_pushstring(L, e.what());
return 2;
} catch (const std::exception& e) {
lua_pushnil(L);
lua_pushstring(L, e.what());
return 2;
}
}
//=============================================================================
// REGISTRATION
//=============================================================================
// Helper to set a global in the real _G (bypassing any proxy)
static void SetGlobalInRealG(lua_State* L, const char* name) {
// Stack: value to set as global
// Get _G (might be a proxy)
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS);
// Check if it has a metatable with __index (proxy pattern)
if (lua_getmetatable(L, -1)) {
lua_getfield(L, -1, "__index");
if (lua_istable(L, -1)) {
// Found real _G through proxy's __index
// Stack: value, proxy, mt, real_G
lua_pushvalue(L, -4); // Copy value
lua_setfield(L, -2, name); // real_G[name] = value
lua_pop(L, 4); // pop real_G, mt, proxy, original value
return;
}
lua_pop(L, 2); // pop __index, metatable
}
// No proxy, set directly in _G
// Stack: value, _G
lua_pushvalue(L, -2); // Copy value
lua_setfield(L, -2, name); // _G[name] = value
lua_pop(L, 2); // pop _G, original value
}
void RegisterJsonAPI(lua_State* L, const JsonLimits& limits) {
// Store limits in registry (allocate static storage)
static JsonLimits stored_limits;
stored_limits = limits;
lua_pushlightuserdata(L, &stored_limits);
lua_setfield(L, LUA_REGISTRYINDEX, JSON_LIMITS_KEY);
// Create json table
lua_newtable(L);
lua_pushcfunction(L, lua_json_decode);
lua_setfield(L, -2, "decode");
lua_pushcfunction(L, lua_json_encode);
lua_setfield(L, -2, "encode");
// Set as global (bypassing proxy)
SetGlobalInRealG(L, "json");
}
} // namespace mosis

View File

@@ -0,0 +1,448 @@
#include "lua_sandbox.h"
#include <lua.hpp>
#include <fstream>
#include <sstream>
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cctype>
namespace mosis {
//=============================================================================
// ALLOCATOR
//=============================================================================
void* LuaSandbox::SandboxAlloc(void* ud, void* ptr, size_t osize, size_t nsize) {
auto* sandbox = static_cast<LuaSandbox*>(ud);
// Calculate new usage
// osize is the old size (0 for new allocations)
// nsize is the new size (0 for frees)
size_t new_usage = sandbox->m_memory_used - osize + nsize;
// Check limit (only when allocating, not freeing)
if (nsize > 0 && new_usage > sandbox->m_limits.memory_bytes) {
// Allocation would exceed limit - return nullptr to signal failure
// Lua will raise a memory error
return nullptr;
}
// Update tracking
sandbox->m_memory_used = new_usage;
// Free operation
if (nsize == 0) {
free(ptr);
return nullptr;
}
// Alloc or realloc
return realloc(ptr, nsize);
}
//=============================================================================
// INSTRUCTION HOOK
//=============================================================================
void LuaSandbox::InstructionHook(lua_State* L, lua_Debug* ar) {
(void)ar; // Unused
// Get sandbox pointer from registry
lua_getfield(L, LUA_REGISTRYINDEX, "__mosis_sandbox");
auto* sandbox = static_cast<LuaSandbox*>(lua_touserdata(L, -1));
lua_pop(L, 1);
if (!sandbox) return;
// Increment by hook interval (called every 1000 instructions)
sandbox->m_instructions_used += 1000;
// Check limit
if (sandbox->m_instructions_used > sandbox->m_limits.instructions_per_call) {
luaL_error(L, "instruction limit exceeded");
}
}
//=============================================================================
// SAFE PRINT
//=============================================================================
int LuaSandbox::SafePrint(lua_State* L) {
int n = lua_gettop(L); // number of arguments
lua_getglobal(L, "tostring");
for (int i = 1; i <= n; i++) {
if (i > 1) std::cout << "\t";
lua_pushvalue(L, -1); // push tostring
lua_pushvalue(L, i); // push argument
lua_call(L, 1, 1); // call tostring
const char* s = lua_tostring(L, -1);
if (s) {
std::cout << s;
}
lua_pop(L, 1);
}
std::cout << std::endl;
return 0;
}
//=============================================================================
// CONSTRUCTOR / DESTRUCTOR
//=============================================================================
LuaSandbox::LuaSandbox(const SandboxContext& context, const SandboxLimits& limits)
: m_context(context), m_limits(limits) {
// Create Lua state with custom allocator
m_L = lua_newstate(SandboxAlloc, this);
if (!m_L) {
m_last_error = "Failed to create Lua state";
return;
}
// Store sandbox pointer in registry for hooks to access
lua_pushlightuserdata(m_L, this);
lua_setfield(m_L, LUA_REGISTRYINDEX, "__mosis_sandbox");
// Setup the sandbox
SetupSandbox();
}
LuaSandbox::~LuaSandbox() {
if (m_L) {
lua_close(m_L);
m_L = nullptr;
}
}
//=============================================================================
// SETUP
//=============================================================================
void LuaSandbox::SetupSandbox() {
// Open safe standard libraries
luaL_openlibs(m_L);
// Remove dangerous globals FIRST
RemoveDangerousGlobals();
// Setup safe replacements
SetupSafeGlobals();
// Protect metatables
ProtectBuiltinTables();
// Setup instruction hook for CPU limiting
SetupInstructionHook();
}
void LuaSandbox::RemoveDangerousGlobals() {
// List of dangerous globals to remove
const char* dangerous_globals[] = {
// Code execution from files/strings
"dofile",
"loadfile",
"load",
"loadstring", // Lua 5.1 compat
// Raw access (bypasses metatables)
"rawget",
"rawset",
"rawequal",
"rawlen",
// Metatable manipulation
// Note: We keep getmetatable but protect the actual metatables
// setmetatable is removed to prevent modifications
"setmetatable",
// GC manipulation
"collectgarbage",
// Dangerous libraries
"os",
"io",
"debug",
"package",
// LuaJIT / FFI (if present)
"ffi",
"jit",
"newproxy",
// Module system (we'll add safe version later)
"require",
nullptr
};
for (const char** p = dangerous_globals; *p; ++p) {
lua_pushnil(m_L);
lua_setglobal(m_L, *p);
}
// Remove string.dump (can create bytecode from functions)
lua_getglobal(m_L, "string");
if (lua_istable(m_L, -1)) {
lua_pushnil(m_L);
lua_setfield(m_L, -2, "dump");
}
lua_pop(m_L, 1);
}
void LuaSandbox::SetupSafeGlobals() {
// Replace print with safe version
lua_pushcfunction(m_L, SafePrint);
lua_setglobal(m_L, "print");
// Setup safe require if app_path is set
if (!m_context.app_path.empty()) {
SetupSafeRequire();
}
}
//=============================================================================
// SAFE REQUIRE
//=============================================================================
// Registry key for loaded modules cache
static const char* LOADED_KEY = "mosis.loaded_modules";
// Validate module name for require() - alphanumeric, underscore, dots only
static bool IsValidModuleName(const std::string& name) {
if (name.empty()) return false;
for (size_t i = 0; i < name.length(); i++) {
char c = name[i];
if (std::isalnum(static_cast<unsigned char>(c))) continue;
if (c == '_') continue;
if (c == '.') {
if (i == 0 || i == name.length() - 1) return false;
if (i > 0 && name[i-1] == '.') return false;
continue;
}
return false;
}
if (name.find("..") != std::string::npos) return false;
return true;
}
// Convert module name to path (e.g., "ui.button" -> "scripts/ui/button.lua")
static std::string ModuleToPath(const std::string& module_name) {
std::string path = module_name;
std::replace(path.begin(), path.end(), '.', '/');
return "scripts/" + path + ".lua";
}
int LuaSandbox::SafeRequire(lua_State* L) {
// Get module name
const char* module_name = luaL_checkstring(L, 1);
// Validate module name
if (!IsValidModuleName(module_name)) {
return luaL_error(L, "invalid module name: %s", module_name);
}
// Check cache first
lua_getfield(L, LUA_REGISTRYINDEX, LOADED_KEY);
if (lua_istable(L, -1)) {
lua_getfield(L, -1, module_name);
if (!lua_isnil(L, -1)) {
return 1; // Return cached module
}
lua_pop(L, 1);
}
lua_pop(L, 1);
// Get sandbox pointer from registry
lua_getfield(L, LUA_REGISTRYINDEX, "__mosis_sandbox");
auto* sandbox = static_cast<LuaSandbox*>(lua_touserdata(L, -1));
lua_pop(L, 1);
if (!sandbox) {
return luaL_error(L, "require not properly initialized");
}
// Build full path
std::string relative_path = ModuleToPath(module_name);
std::string full_path = sandbox->m_context.app_path;
if (!full_path.empty() && full_path.back() != '/' && full_path.back() != '\\') {
full_path += '/';
}
full_path += relative_path;
// Read the file
std::ifstream file(full_path);
if (!file.is_open()) {
return luaL_error(L, "module '%s' not found at '%s'", module_name, full_path.c_str());
}
std::stringstream buffer;
buffer << file.rdbuf();
std::string source = buffer.str();
file.close();
// Load as text only (no bytecode)
std::string chunk_name = "@" + std::string(module_name);
int status = luaL_loadbufferx(L, source.c_str(), source.size(),
chunk_name.c_str(), "t");
if (status != LUA_OK) {
return lua_error(L);
}
// Execute the chunk
lua_call(L, 0, 1);
// If chunk returned nil, use true as the module value
if (lua_isnil(L, -1)) {
lua_pop(L, 1);
lua_pushboolean(L, 1);
}
// Cache the result
lua_getfield(L, LUA_REGISTRYINDEX, LOADED_KEY);
if (!lua_istable(L, -1)) {
lua_pop(L, 1);
lua_newtable(L);
lua_pushvalue(L, -1);
lua_setfield(L, LUA_REGISTRYINDEX, LOADED_KEY);
}
lua_pushvalue(L, -2);
lua_setfield(L, -2, module_name);
lua_pop(L, 1);
return 1;
}
void LuaSandbox::SetupSafeRequire() {
// Create loaded modules cache
lua_newtable(m_L);
lua_setfield(m_L, LUA_REGISTRYINDEX, LOADED_KEY);
// Register require function
lua_pushcfunction(m_L, SafeRequire);
lua_setglobal(m_L, "require");
}
void LuaSandbox::ProtectBuiltinTables() {
// Protect string metatable
// When someone calls getmetatable(""), they get "string" instead of the real metatable
lua_pushstring(m_L, "");
if (lua_getmetatable(m_L, -1)) {
lua_pushstring(m_L, "string");
lua_setfield(m_L, -2, "__metatable");
lua_pop(m_L, 1); // pop metatable
}
lua_pop(m_L, 1); // pop string
// Freeze _G using a proxy pattern
// This is needed because __newindex only fires for NEW keys, not existing ones
// We create: empty_proxy -> metatable { __index = real_G, __newindex = error }
// Get the current _G (with all our safe functions)
lua_pushglobaltable(m_L); // stack: real_G
// Create a new empty table to be the proxy
lua_newtable(m_L); // stack: real_G, proxy
// Create metatable for proxy
lua_newtable(m_L); // stack: real_G, proxy, mt
// __metatable - prevent access to real metatable
lua_pushstring(m_L, "globals");
lua_setfield(m_L, -2, "__metatable");
// __index - read from real_G
lua_pushvalue(m_L, -3); // push real_G
lua_setfield(m_L, -2, "__index");
// __newindex - block all writes
lua_pushcfunction(m_L, [](lua_State* L) -> int {
const char* key = lua_tostring(L, 2);
return luaL_error(L, "cannot modify global '%s'", key ? key : "(unknown)");
});
lua_setfield(m_L, -2, "__newindex");
// Set metatable on proxy: setmetatable(proxy, mt)
lua_setmetatable(m_L, -2); // stack: real_G, proxy
// Now we need to replace _G with proxy
// In Lua 5.2+, we use lua_rawseti on the registry
lua_pushvalue(m_L, -1); // stack: real_G, proxy, proxy
lua_rawseti(m_L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS); // stack: real_G, proxy
// Also update _G variable in real_G to point to proxy
// This is critical: when code does _G.foo = bar, it accesses _G variable
lua_setfield(m_L, -2, "_G"); // stack: real_G (sets real_G["_G"] = proxy)
lua_pop(m_L, 1); // pop real_G
}
void LuaSandbox::SetupInstructionHook() {
// Set hook to fire every 1000 VM instructions
lua_sethook(m_L, InstructionHook, LUA_MASKCOUNT, 1000);
}
//=============================================================================
// LOAD AND EXECUTE
//=============================================================================
bool LuaSandbox::LoadString(const std::string& code, const std::string& chunk_name) {
if (!m_L) {
m_last_error = "Lua state not initialized";
return false;
}
// Reset instruction count for this execution
ResetInstructionCount();
// Load as TEXT ONLY - "t" mode rejects bytecode (starts with \x1bLua)
int result = luaL_loadbufferx(m_L, code.c_str(), code.size(),
chunk_name.c_str(), "t");
if (result != LUA_OK) {
m_last_error = lua_tostring(m_L, -1);
lua_pop(m_L, 1);
return false;
}
// Execute the loaded chunk
result = lua_pcall(m_L, 0, 0, 0);
if (result != LUA_OK) {
m_last_error = lua_tostring(m_L, -1);
lua_pop(m_L, 1);
return false;
}
m_last_error.clear();
return true;
}
bool LuaSandbox::LoadFile(const std::string& path) {
// Read file contents
std::ifstream f(path);
if (!f) {
m_last_error = "Cannot open file: " + path;
return false;
}
std::stringstream ss;
ss << f.rdbuf();
std::string code = ss.str();
// Load as string
return LoadString(code, "@" + path);
}
void LuaSandbox::ResetInstructionCount() {
m_instructions_used = 0;
}
} // namespace mosis

View File

@@ -0,0 +1,249 @@
#include "network_manager.h"
#include <lua.hpp>
#include <algorithm>
namespace mosis {
NetworkManager::NetworkManager(const std::string& app_id, const NetworkLimits& limits)
: m_app_id(app_id)
, m_limits(limits)
, m_mock_mode(true)
{
}
NetworkManager::~NetworkManager() {
}
void NetworkManager::SetAllowedDomains(const std::vector<std::string>& domains) {
m_validator.SetAllowedDomains(domains);
}
void NetworkManager::ClearDomainRestrictions() {
m_validator.ClearDomainRestrictions();
}
bool NetworkManager::ValidateRequest(const HttpRequest& request, std::string& error) {
// Validate URL
auto parsed = m_validator.Validate(request.url, error);
if (!parsed) {
return false;
}
// Validate method
std::string method = request.method;
std::transform(method.begin(), method.end(), method.begin(),
[](unsigned char c) { return std::toupper(c); });
static const std::vector<std::string> allowed_methods = {
"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"
};
bool method_valid = false;
for (const auto& m : allowed_methods) {
if (method == m) {
method_valid = true;
break;
}
}
if (!method_valid) {
error = "Invalid HTTP method: " + request.method;
return false;
}
// Validate request body size
if (request.body.size() > m_limits.max_request_body) {
error = "Request body too large: " + std::to_string(request.body.size()) +
" bytes (max " + std::to_string(m_limits.max_request_body) + ")";
return false;
}
// Validate timeout
if (request.timeout_ms > m_limits.max_timeout_ms) {
error = "Timeout too large: " + std::to_string(request.timeout_ms) +
"ms (max " + std::to_string(m_limits.max_timeout_ms) + "ms)";
return false;
}
// Check concurrent request limit
if (m_active_requests.load() >= m_limits.max_concurrent_requests) {
error = "Too many concurrent requests (max " +
std::to_string(m_limits.max_concurrent_requests) + ")";
return false;
}
return true;
}
HttpResponse NetworkManager::Request(const HttpRequest& request, std::string& error) {
HttpResponse response;
// Validate the request
if (!ValidateRequest(request, error)) {
response.error = error;
return response;
}
// In mock mode, we don't actually make network calls
// This is for testing the validation logic
if (m_mock_mode) {
error = "Network requests disabled in mock mode";
response.error = error;
return response;
}
// Track active requests
m_active_requests++;
// In a real implementation, we would make the HTTP request here
// For now, just return an error indicating no network implementation
error = "Network requests not implemented on this platform";
response.error = error;
m_active_requests--;
return response;
}
int NetworkManager::GetActiveRequestCount() const {
return m_active_requests.load();
}
// Lua API implementation
// Get NetworkManager from upvalue
static NetworkManager* GetManager(lua_State* L) {
return static_cast<NetworkManager*>(lua_touserdata(L, lua_upvalueindex(1)));
}
// network.request(options) -> response, error
static int L_network_request(lua_State* L) {
NetworkManager* manager = GetManager(L);
if (!manager) {
lua_pushnil(L);
lua_pushstring(L, "NetworkManager not available");
return 2;
}
// Expect table argument
luaL_checktype(L, 1, LUA_TTABLE);
HttpRequest request;
// Get URL (required)
lua_getfield(L, 1, "url");
if (!lua_isstring(L, -1)) {
lua_pushnil(L);
lua_pushstring(L, "url is required and must be a string");
return 2;
}
request.url = lua_tostring(L, -1);
lua_pop(L, 1);
// Get method (optional, default GET)
lua_getfield(L, 1, "method");
if (lua_isstring(L, -1)) {
request.method = lua_tostring(L, -1);
}
lua_pop(L, 1);
// Get headers (optional)
lua_getfield(L, 1, "headers");
if (lua_istable(L, -1)) {
lua_pushnil(L);
while (lua_next(L, -2) != 0) {
if (lua_isstring(L, -2) && lua_isstring(L, -1)) {
request.headers[lua_tostring(L, -2)] = lua_tostring(L, -1);
}
lua_pop(L, 1);
}
}
lua_pop(L, 1);
// Get body (optional)
lua_getfield(L, 1, "body");
if (lua_isstring(L, -1)) {
size_t len;
const char* body = lua_tolstring(L, -1, &len);
request.body = std::string(body, len);
}
lua_pop(L, 1);
// Get timeout (optional)
lua_getfield(L, 1, "timeout");
if (lua_isnumber(L, -1)) {
request.timeout_ms = static_cast<int>(lua_tointeger(L, -1));
}
lua_pop(L, 1);
// Make request
std::string error;
HttpResponse response = manager->Request(request, error);
if (!error.empty()) {
lua_pushnil(L);
lua_pushstring(L, error.c_str());
return 2;
}
// Return response as table
lua_newtable(L);
lua_pushinteger(L, response.status_code);
lua_setfield(L, -2, "status");
lua_pushstring(L, response.body.c_str());
lua_setfield(L, -2, "body");
// Headers table
lua_newtable(L);
for (const auto& [key, value] : response.headers) {
lua_pushstring(L, value.c_str());
lua_setfield(L, -2, key.c_str());
}
lua_setfield(L, -2, "headers");
if (!response.error.empty()) {
lua_pushstring(L, response.error.c_str());
lua_setfield(L, -2, "error");
}
return 1; // Return response table
}
// Helper to set a global in the real _G (bypassing any proxy)
static void SetGlobalInRealG(lua_State* L, const char* name) {
// Stack: value to set as global
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS);
// Check if it's a proxy with __index pointing to real _G
if (lua_getmetatable(L, -1)) {
lua_getfield(L, -1, "__index");
if (lua_istable(L, -1)) {
// This is the real _G, set our value there
lua_pushvalue(L, -4); // Push the value
lua_setfield(L, -2, name);
lua_pop(L, 4); // Pop __index, metatable, proxy, (value already consumed)
return;
}
lua_pop(L, 2); // Pop __index and metatable
}
// No proxy, set directly
lua_pushvalue(L, -2); // Push the value
lua_setfield(L, -2, name);
lua_pop(L, 2); // Pop globals table and original value
}
void RegisterNetworkAPI(lua_State* L, NetworkManager* manager) {
// Create network table
lua_newtable(L);
// Add request function with manager as upvalue
lua_pushlightuserdata(L, manager);
lua_pushcclosure(L, L_network_request, 1);
lua_setfield(L, -2, "request");
// Set as global
SetGlobalInRealG(L, "network");
}
} // namespace mosis

View File

@@ -0,0 +1,344 @@
#include "path_sandbox.h"
#include <lua.hpp>
#include <algorithm>
#include <cctype>
#include <fstream>
#include <sstream>
namespace mosis {
//=============================================================================
// CONSTRUCTOR
//=============================================================================
PathSandbox::PathSandbox(const std::string& app_path)
: m_app_path(app_path)
{
// Normalize the app path
if (!m_app_path.empty()) {
// Ensure trailing separator for prefix matching
if (m_app_path.back() != '/' && m_app_path.back() != '\\') {
m_app_path += '/';
}
// Normalize separators to forward slash
std::replace(m_app_path.begin(), m_app_path.end(), '\\', '/');
}
}
//=============================================================================
// PATH VALIDATION
//=============================================================================
bool PathSandbox::ContainsTraversal(const std::string& path) {
std::string normalized = NormalizePath(path);
// Check for .. anywhere in the path
size_t pos = 0;
while ((pos = normalized.find("..", pos)) != std::string::npos) {
// Make sure it's actually a parent directory reference, not part of a filename
bool at_start = (pos == 0);
bool before_is_sep = (pos > 0 && (normalized[pos-1] == '/' || normalized[pos-1] == '\\'));
size_t after_pos = pos + 2;
bool at_end = (after_pos >= normalized.size());
bool after_is_sep = (!at_end && (normalized[after_pos] == '/' || normalized[after_pos] == '\\'));
if ((at_start || before_is_sep) && (at_end || after_is_sep)) {
return true;
}
pos++;
}
return false;
}
bool PathSandbox::IsAbsolutePath(const std::string& path) {
if (path.empty()) return false;
// Unix absolute path
if (path[0] == '/') return true;
// Windows absolute path (C:\ or C:/)
if (path.length() >= 2) {
char first = path[0];
if (std::isalpha(static_cast<unsigned char>(first)) && path[1] == ':') {
return true;
}
}
// UNC path (\\server\share or //server/share)
if (path.length() >= 2) {
if ((path[0] == '\\' && path[1] == '\\') ||
(path[0] == '/' && path[1] == '/')) {
return true;
}
}
return false;
}
std::string PathSandbox::NormalizePath(const std::string& path) {
std::string result = path;
// Convert backslashes to forward slashes
std::replace(result.begin(), result.end(), '\\', '/');
// Remove leading ./
while (result.length() >= 2 && result[0] == '.' && result[1] == '/') {
result = result.substr(2);
}
// Remove duplicate slashes
std::string cleaned;
bool last_was_slash = false;
for (char c : result) {
if (c == '/') {
if (!last_was_slash) {
cleaned += c;
}
last_was_slash = true;
} else {
cleaned += c;
last_was_slash = false;
}
}
return cleaned;
}
bool PathSandbox::ValidatePath(const std::string& path, std::string& out_canonical) {
// Reject empty paths
if (path.empty()) {
return false;
}
// Reject absolute paths
if (IsAbsolutePath(path)) {
return false;
}
// Reject traversal attempts
if (ContainsTraversal(path)) {
return false;
}
// Normalize and resolve the path
std::string normalized = NormalizePath(path);
std::string resolved = ResolvePath(normalized);
// Use filesystem to get canonical path (resolves any remaining .)
try {
std::filesystem::path fs_path(resolved);
// If the file exists, use canonical path for strict checking
if (std::filesystem::exists(fs_path)) {
std::filesystem::path canonical = std::filesystem::canonical(fs_path);
std::string canonical_str = canonical.string();
std::replace(canonical_str.begin(), canonical_str.end(), '\\', '/');
// Verify the canonical path is still within app_path
std::string app_canonical = std::filesystem::canonical(
std::filesystem::path(m_app_path)).string();
std::replace(app_canonical.begin(), app_canonical.end(), '\\', '/');
if (!app_canonical.empty() && app_canonical.back() != '/') {
app_canonical += '/';
}
if (canonical_str.rfind(app_canonical, 0) != 0) {
return false; // Path escaped sandbox via symlink
}
out_canonical = canonical_str;
} else {
// File doesn't exist, just use the resolved path
out_canonical = resolved;
}
} catch (const std::filesystem::filesystem_error&) {
// Filesystem error, use the resolved path as-is
out_canonical = resolved;
}
return true;
}
std::string PathSandbox::ResolvePath(const std::string& relative_path) {
std::string normalized = NormalizePath(relative_path);
// Combine with app path
std::string result = m_app_path + normalized;
return result;
}
//=============================================================================
// MODULE NAME VALIDATION
//=============================================================================
bool PathSandbox::IsValidModuleName(const std::string& name) {
if (name.empty()) {
return false;
}
// Check each character
for (size_t i = 0; i < name.length(); i++) {
char c = name[i];
// Allow alphanumeric
if (std::isalnum(static_cast<unsigned char>(c))) {
continue;
}
// Allow underscore
if (c == '_') {
continue;
}
// Allow dot for submodules, but not at start/end or consecutive
if (c == '.') {
if (i == 0 || i == name.length() - 1) {
return false; // Dot at start or end
}
if (i > 0 && name[i-1] == '.') {
return false; // Consecutive dots
}
continue;
}
// Any other character is invalid
return false;
}
// Reject names that look like traversal
if (name.find("..") != std::string::npos) {
return false;
}
return true;
}
std::string PathSandbox::ModuleToPath(const std::string& module_name) {
// Convert dots to path separators
std::string path = module_name;
std::replace(path.begin(), path.end(), '.', '/');
// Add scripts/ prefix and .lua suffix
return "scripts/" + path + ".lua";
}
//=============================================================================
// SAFE REQUIRE
//=============================================================================
// Registry key for PathSandbox pointer
static const char* SANDBOX_KEY = "mosis.path_sandbox";
// Registry key for loaded modules cache
static const char* LOADED_KEY = "mosis.loaded_modules";
int SafeRequire(lua_State* L) {
// Get module name
const char* module_name = luaL_checkstring(L, 1);
// Validate module name
if (!PathSandbox::IsValidModuleName(module_name)) {
return luaL_error(L, "invalid module name: %s", module_name);
}
// Check cache first
lua_getfield(L, LUA_REGISTRYINDEX, LOADED_KEY);
if (lua_istable(L, -1)) {
lua_getfield(L, -1, module_name);
if (!lua_isnil(L, -1)) {
// Module already loaded, return cached value
return 1;
}
lua_pop(L, 1); // Pop nil
}
lua_pop(L, 1); // Pop cache table (or nil if not exists)
// Get PathSandbox from registry
lua_getfield(L, LUA_REGISTRYINDEX, SANDBOX_KEY);
if (!lua_islightuserdata(L, -1)) {
return luaL_error(L, "require not properly initialized");
}
PathSandbox* sandbox = static_cast<PathSandbox*>(lua_touserdata(L, -1));
lua_pop(L, 1);
// Convert module name to path
std::string relative_path = PathSandbox::ModuleToPath(module_name);
// Validate the path
std::string canonical;
if (!sandbox->ValidatePath(relative_path, canonical)) {
return luaL_error(L, "cannot load module '%s': path validation failed", module_name);
}
// Read the file
std::ifstream file(canonical);
if (!file.is_open()) {
// Try with the resolved path directly (in case canonical check failed)
std::string resolved = sandbox->ResolvePath(relative_path);
file.open(resolved);
if (!file.is_open()) {
return luaL_error(L, "module '%s' not found", module_name);
}
}
std::stringstream buffer;
buffer << file.rdbuf();
std::string source = buffer.str();
file.close();
// Load as text only (no bytecode)
std::string chunk_name = "@" + std::string(module_name);
int status = luaL_loadbufferx(L, source.c_str(), source.size(),
chunk_name.c_str(), "t");
if (status != LUA_OK) {
return lua_error(L); // Propagate error
}
// Execute the chunk
lua_call(L, 0, 1);
// If chunk returned nil, use true as the module value
if (lua_isnil(L, -1)) {
lua_pop(L, 1);
lua_pushboolean(L, 1);
}
// Cache the result
lua_getfield(L, LUA_REGISTRYINDEX, LOADED_KEY);
if (!lua_istable(L, -1)) {
// Create cache table if it doesn't exist
lua_pop(L, 1);
lua_newtable(L);
lua_pushvalue(L, -1);
lua_setfield(L, LUA_REGISTRYINDEX, LOADED_KEY);
}
// cache[module_name] = result
lua_pushvalue(L, -2); // Push the result
lua_setfield(L, -2, module_name);
lua_pop(L, 1); // Pop cache table
// Return the module
return 1;
}
void RegisterSafeRequire(lua_State* L, PathSandbox* sandbox) {
// Store PathSandbox pointer in registry
lua_pushlightuserdata(L, sandbox);
lua_setfield(L, LUA_REGISTRYINDEX, SANDBOX_KEY);
// Create loaded modules cache
lua_newtable(L);
lua_setfield(L, LUA_REGISTRYINDEX, LOADED_KEY);
// Register require function
lua_pushcfunction(L, SafeRequire);
lua_setglobal(L, "require");
}
} // namespace mosis

View File

@@ -0,0 +1,197 @@
#include "permission_gate.h"
#include "lua_sandbox.h"
#include <lua.hpp>
#include <algorithm>
namespace mosis {
//=============================================================================
// PERMISSION DATABASE
//=============================================================================
static const std::unordered_map<std::string, PermissionInfo> PERMISSIONS = {
// Normal permissions (auto-granted when declared)
{"internet", {PermissionCategory::Normal, "Access the internet"}},
{"vibrate", {PermissionCategory::Normal, "Vibrate the device"}},
{"wake_lock", {PermissionCategory::Normal, "Keep device awake"}},
{"notifications", {PermissionCategory::Normal, "Show notifications"}},
{"alarms", {PermissionCategory::Normal, "Set alarms"}},
{"nfc", {PermissionCategory::Normal, "Use NFC"}},
// Dangerous permissions (require user consent)
{"camera", {PermissionCategory::Dangerous, "Access the camera"}},
{"microphone", {PermissionCategory::Dangerous, "Record audio"}},
{"location.fine", {PermissionCategory::Dangerous, "Access precise location"}},
{"location.coarse", {PermissionCategory::Dangerous, "Access approximate location"}},
{"contacts.read", {PermissionCategory::Dangerous, "Read contacts"}},
{"contacts.write", {PermissionCategory::Dangerous, "Modify contacts"}},
{"storage.external", {PermissionCategory::Dangerous, "Access external storage"}},
{"storage.shared", {PermissionCategory::Dangerous, "Access shared storage"}},
{"sensors.motion", {PermissionCategory::Dangerous, "Access motion sensors"}},
{"bluetooth", {PermissionCategory::Dangerous, "Use Bluetooth"}},
{"bluetooth.scan", {PermissionCategory::Dangerous, "Scan for Bluetooth devices"}},
{"calendar.read", {PermissionCategory::Dangerous, "Read calendar"}},
{"calendar.write", {PermissionCategory::Dangerous, "Modify calendar"}},
{"phone.call", {PermissionCategory::Dangerous, "Make phone calls"}},
{"phone.read_state", {PermissionCategory::Dangerous, "Read phone state"}},
{"sms.read", {PermissionCategory::Dangerous, "Read SMS messages"}},
{"sms.send", {PermissionCategory::Dangerous, "Send SMS messages"}},
// Signature permissions (system apps only)
{"system.settings", {PermissionCategory::Signature, "Modify system settings"}},
{"system.install", {PermissionCategory::Signature, "Install apps"}},
{"system.uninstall", {PermissionCategory::Signature, "Uninstall apps"}},
{"system.admin", {PermissionCategory::Signature, "Device administrator"}},
{"system.overlay", {PermissionCategory::Signature, "Display over other apps"}},
{"system.wallpaper", {PermissionCategory::Signature, "Set wallpaper"}},
};
//=============================================================================
// CONSTRUCTOR
//=============================================================================
PermissionGate::PermissionGate(const SandboxContext& context)
: m_context(context)
, m_last_gesture(std::chrono::steady_clock::time_point::min())
{
}
//=============================================================================
// PERMISSION INFO
//=============================================================================
PermissionCategory PermissionGate::GetCategory(const std::string& permission) {
auto it = PERMISSIONS.find(permission);
if (it != PERMISSIONS.end()) {
return it->second.category;
}
// Unknown permissions default to Dangerous for safety
return PermissionCategory::Dangerous;
}
const PermissionInfo* PermissionGate::GetPermissionInfo(const std::string& permission) {
auto it = PERMISSIONS.find(permission);
if (it != PERMISSIONS.end()) {
return &it->second;
}
return nullptr;
}
//=============================================================================
// PERMISSION CHECKING
//=============================================================================
bool PermissionGate::HasPermission(const std::string& permission) const {
auto category = GetCategory(permission);
switch (category) {
case PermissionCategory::Normal:
return CheckNormalPermission(permission);
case PermissionCategory::Dangerous:
return CheckDangerousPermission(permission);
case PermissionCategory::Signature:
return CheckSignaturePermission(permission);
}
return false;
}
bool PermissionGate::Check(lua_State* L, const std::string& permission) {
if (!HasPermission(permission)) {
luaL_error(L, "permission denied: %s", permission.c_str());
return false;
}
return true;
}
bool PermissionGate::IsDeclared(const std::string& permission) const {
const auto& declared = m_context.permissions;
return std::find(declared.begin(), declared.end(), permission) != declared.end();
}
bool PermissionGate::CheckNormalPermission(const std::string& permission) const {
// Normal permissions are auto-granted if declared in manifest
return IsDeclared(permission);
}
bool PermissionGate::CheckDangerousPermission(const std::string& permission) const {
// Must be declared in manifest
if (!IsDeclared(permission)) {
return false;
}
// System apps get dangerous permissions automatically
if (m_context.is_system_app) {
return true;
}
// Regular apps need runtime grant
return m_runtime_grants.count(permission) > 0;
}
bool PermissionGate::CheckSignaturePermission(const std::string& permission) const {
// Only system apps get signature permissions
if (!m_context.is_system_app) {
return false;
}
// Must still be declared
return IsDeclared(permission);
}
//=============================================================================
// USER GESTURE TRACKING
//=============================================================================
void PermissionGate::RecordUserGesture() {
m_last_gesture = std::chrono::steady_clock::now();
}
bool PermissionGate::HasRecentUserGesture(int ms) const {
// If no gesture has been recorded, return false
if (m_last_gesture == std::chrono::steady_clock::time_point::min()) {
return false;
}
auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_last_gesture);
return elapsed.count() < ms;
}
//=============================================================================
// RUNTIME GRANTS
//=============================================================================
void PermissionGate::GrantPermission(const std::string& permission) {
// Can only grant dangerous permissions
auto category = GetCategory(permission);
if (category == PermissionCategory::Dangerous) {
m_runtime_grants.insert(permission);
}
}
void PermissionGate::RevokePermission(const std::string& permission) {
m_runtime_grants.erase(permission);
}
//=============================================================================
// QUERIES
//=============================================================================
const std::vector<std::string>& PermissionGate::GetDeclaredPermissions() const {
return m_context.permissions;
}
std::vector<std::string> PermissionGate::GetGrantedPermissions() const {
std::vector<std::string> granted;
for (const auto& perm : m_context.permissions) {
if (HasPermission(perm)) {
granted.push_back(perm);
}
}
return granted;
}
} // namespace mosis

View File

@@ -0,0 +1,209 @@
#include "rate_limiter.h"
#include <algorithm>
namespace mosis {
//=============================================================================
// CONSTRUCTOR (with default limits)
//=============================================================================
RateLimiter::RateLimiter() {
// Network operations
SetLimit("network.request", {10.0, 100.0}); // 10/sec, burst 100
SetLimit("network.websocket", {2.0, 10.0}); // 2/sec, burst 10
SetLimit("network.download", {5.0, 20.0}); // 5/sec, burst 20
// Storage operations
SetLimit("storage.read", {100.0, 500.0}); // 100/sec, burst 500
SetLimit("storage.write", {20.0, 100.0}); // 20/sec, burst 100
SetLimit("storage.delete", {10.0, 50.0}); // 10/sec, burst 50
SetLimit("database.query", {50.0, 200.0}); // 50/sec, burst 200
// Hardware access
SetLimit("camera.capture", {30.0, 30.0}); // 30 fps max
SetLimit("microphone.record", {1.0, 1.0}); // 1 session at a time
SetLimit("location.request", {1.0, 5.0}); // 1/sec, burst 5
SetLimit("sensor.read", {60.0, 60.0}); // 60 Hz max
// Timers
SetLimit("timer.create", {10.0, 100.0}); // 10/sec, burst 100
// Crypto
SetLimit("crypto.random", {100.0, 1000.0}); // 100/sec, burst 1000
SetLimit("crypto.hash", {100.0, 1000.0}); // 100/sec, burst 1000
}
//=============================================================================
// CONFIGURATION
//=============================================================================
void RateLimiter::SetLimit(const std::string& operation, const RateLimitConfig& config) {
std::lock_guard<std::mutex> lock(m_mutex);
m_configs[operation] = config;
}
const RateLimitConfig* RateLimiter::GetLimit(const std::string& operation) const {
std::lock_guard<std::mutex> lock(m_mutex);
auto it = m_configs.find(operation);
if (it != m_configs.end()) {
return &it->second;
}
return nullptr;
}
//=============================================================================
// CHECKING
//=============================================================================
bool RateLimiter::Check(const std::string& app_id, const std::string& operation) {
std::lock_guard<std::mutex> lock(m_mutex);
// Find config
auto config_it = m_configs.find(operation);
if (config_it == m_configs.end()) {
// No limit configured, allow by default
return true;
}
const auto& config = config_it->second;
auto& bucket = GetBucket(app_id, operation);
// Refill based on elapsed time
Refill(bucket, config);
// Check if we have a token
if (bucket.tokens >= 1.0) {
bucket.tokens -= 1.0;
return true;
}
return false;
}
bool RateLimiter::CanProceed(const std::string& app_id, const std::string& operation) const {
std::lock_guard<std::mutex> lock(m_mutex);
// Find config
auto config_it = m_configs.find(operation);
if (config_it == m_configs.end()) {
return true; // No limit
}
const auto& config = config_it->second;
std::string key = MakeKey(app_id, operation);
auto bucket_it = m_buckets.find(key);
if (bucket_it == m_buckets.end()) {
return true; // New bucket would have full tokens
}
// Make a copy to check without modifying
Bucket bucket = bucket_it->second;
Refill(bucket, config);
return bucket.tokens >= 1.0;
}
double RateLimiter::GetTokens(const std::string& app_id, const std::string& operation) const {
std::lock_guard<std::mutex> lock(m_mutex);
std::string key = MakeKey(app_id, operation);
auto bucket_it = m_buckets.find(key);
if (bucket_it == m_buckets.end()) {
// Check if there's a config
auto config_it = m_configs.find(operation);
if (config_it != m_configs.end()) {
return config_it->second.max_tokens; // Would start with full
}
return 0.0;
}
// Find config to refill
auto config_it = m_configs.find(operation);
if (config_it == m_configs.end()) {
return bucket_it->second.tokens;
}
// Make a copy to check without modifying
Bucket bucket = bucket_it->second;
Refill(bucket, config_it->second);
return bucket.tokens;
}
//=============================================================================
// RESET
//=============================================================================
void RateLimiter::ResetApp(const std::string& app_id) {
std::lock_guard<std::mutex> lock(m_mutex);
// Find and remove all buckets for this app
std::string prefix = app_id + ":";
for (auto it = m_buckets.begin(); it != m_buckets.end(); ) {
if (it->first.rfind(prefix, 0) == 0) { // starts with app_id:
it = m_buckets.erase(it);
} else {
++it;
}
}
}
void RateLimiter::ClearAll() {
std::lock_guard<std::mutex> lock(m_mutex);
m_buckets.clear();
}
//=============================================================================
// INTERNAL
//=============================================================================
void RateLimiter::Refill(Bucket& bucket, const RateLimitConfig& config) const {
auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration<double>(now - bucket.last_refill);
// Add tokens based on elapsed time
double new_tokens = bucket.tokens + (elapsed.count() * config.tokens_per_second);
// Cap at max
bucket.tokens = std::min(new_tokens, config.max_tokens);
bucket.last_refill = now;
}
RateLimiter::Bucket& RateLimiter::GetBucket(const std::string& app_id,
const std::string& operation) {
std::string key = MakeKey(app_id, operation);
auto it = m_buckets.find(key);
if (it != m_buckets.end()) {
return it->second;
}
// Create new bucket with full tokens
auto config_it = m_configs.find(operation);
double initial = (config_it != m_configs.end()) ? config_it->second.max_tokens : 1.0;
m_buckets[key] = Bucket{
.tokens = initial,
.last_refill = std::chrono::steady_clock::now()
};
return m_buckets[key];
}
std::string RateLimiter::MakeKey(const std::string& app_id, const std::string& operation) {
return app_id + ":" + operation;
}
//=============================================================================
// GLOBAL INSTANCE
//=============================================================================
RateLimiter& GetRateLimiter() {
static RateLimiter instance;
return instance;
}
} // namespace mosis

View File

@@ -0,0 +1,440 @@
#include "timer_manager.h"
#include <lua.hpp>
#include <algorithm>
namespace mosis {
//=============================================================================
// CONSTRUCTOR / DESTRUCTOR
//=============================================================================
TimerManager::TimerManager() = default;
TimerManager::~TimerManager() {
std::lock_guard<std::mutex> lock(m_mutex);
// Release all Lua callback references
for (auto& timer : m_timers) {
if (timer.callback_ref != LUA_NOREF && timer.L) {
luaL_unref(timer.L, LUA_REGISTRYINDEX, timer.callback_ref);
}
}
m_timers.clear();
}
//=============================================================================
// TIMER CREATION
//=============================================================================
TimerId TimerManager::SetTimeout(lua_State* L, const std::string& app_id,
int callback_ref, int delay_ms) {
std::lock_guard<std::mutex> lock(m_mutex);
// Check per-app limit
if (m_app_timer_counts[app_id] >= MAX_TIMERS_PER_APP) {
// Release the callback reference since we're not using it
luaL_unref(L, LUA_REGISTRYINDEX, callback_ref);
return 0;
}
// Clamp delay
if (delay_ms < MIN_TIMEOUT_MS) {
delay_ms = MIN_TIMEOUT_MS;
}
Timer timer;
timer.id = m_next_id++;
timer.app_id = app_id;
timer.fire_time = std::chrono::steady_clock::now() + Duration(delay_ms);
timer.interval = Duration(0);
timer.callback_ref = callback_ref;
timer.L = L;
timer.cancelled = false;
timer.is_interval = false;
m_timers.push_back(timer);
m_app_timer_counts[app_id]++;
m_app_timer_ids[app_id].insert(timer.id);
return timer.id;
}
TimerId TimerManager::SetInterval(lua_State* L, const std::string& app_id,
int callback_ref, int interval_ms) {
std::lock_guard<std::mutex> lock(m_mutex);
// Check per-app limit
if (m_app_timer_counts[app_id] >= MAX_TIMERS_PER_APP) {
luaL_unref(L, LUA_REGISTRYINDEX, callback_ref);
return 0;
}
// Clamp interval to minimum
if (interval_ms < MIN_INTERVAL_MS) {
interval_ms = MIN_INTERVAL_MS;
}
Timer timer;
timer.id = m_next_id++;
timer.app_id = app_id;
timer.fire_time = std::chrono::steady_clock::now() + Duration(interval_ms);
timer.interval = Duration(interval_ms);
timer.callback_ref = callback_ref;
timer.L = L;
timer.cancelled = false;
timer.is_interval = true;
m_timers.push_back(timer);
m_app_timer_counts[app_id]++;
m_app_timer_ids[app_id].insert(timer.id);
return timer.id;
}
//=============================================================================
// TIMER CANCELLATION
//=============================================================================
bool TimerManager::ClearTimer(const std::string& app_id, TimerId id) {
std::lock_guard<std::mutex> lock(m_mutex);
// Find the timer
auto it = std::find_if(m_timers.begin(), m_timers.end(),
[id, &app_id](const Timer& t) {
return t.id == id && t.app_id == app_id && !t.cancelled;
});
if (it == m_timers.end()) {
return false;
}
// Mark as cancelled (will be removed during ProcessTimers)
it->cancelled = true;
// Release the Lua callback reference
if (it->callback_ref != LUA_NOREF && it->L) {
luaL_unref(it->L, LUA_REGISTRYINDEX, it->callback_ref);
it->callback_ref = LUA_NOREF;
}
// Update counts
if (m_app_timer_counts[app_id] > 0) {
m_app_timer_counts[app_id]--;
}
m_app_timer_ids[app_id].erase(id);
return true;
}
void TimerManager::ClearAppTimers(const std::string& app_id) {
std::lock_guard<std::mutex> lock(m_mutex);
// Get all timer IDs for this app
auto it = m_app_timer_ids.find(app_id);
if (it == m_app_timer_ids.end()) {
return;
}
// Mark all timers as cancelled and release references
for (auto& timer : m_timers) {
if (timer.app_id == app_id && !timer.cancelled) {
timer.cancelled = true;
if (timer.callback_ref != LUA_NOREF && timer.L) {
luaL_unref(timer.L, LUA_REGISTRYINDEX, timer.callback_ref);
timer.callback_ref = LUA_NOREF;
}
}
}
// Clear tracking
m_app_timer_counts[app_id] = 0;
m_app_timer_ids[app_id].clear();
}
//=============================================================================
// TIMER PROCESSING
//=============================================================================
void TimerManager::FireTimer(Timer& timer) {
if (timer.cancelled || timer.callback_ref == LUA_NOREF || !timer.L) {
return;
}
lua_State* L = timer.L;
// Get the callback from registry
lua_rawgeti(L, LUA_REGISTRYINDEX, timer.callback_ref);
if (lua_isfunction(L, -1)) {
// Call the callback with protected call
int result = lua_pcall(L, 0, 0, 0);
if (result != LUA_OK) {
// Log error but don't propagate
lua_pop(L, 1);
}
} else {
lua_pop(L, 1);
}
}
void TimerManager::RescheduleInterval(Timer& timer) {
// Update fire time for next interval
timer.fire_time = std::chrono::steady_clock::now() + timer.interval;
}
int TimerManager::ProcessTimers() {
// We need to be careful here - firing a timer might cause
// new timers to be added or timers to be cancelled
std::vector<Timer> to_fire;
std::vector<TimerId> to_reschedule;
std::vector<TimerId> to_remove;
auto now = std::chrono::steady_clock::now();
{
std::lock_guard<std::mutex> lock(m_mutex);
// Find all timers that should fire
for (auto& timer : m_timers) {
if (timer.cancelled) {
to_remove.push_back(timer.id);
} else if (timer.fire_time <= now) {
to_fire.push_back(timer);
if (timer.is_interval) {
to_reschedule.push_back(timer.id);
} else {
to_remove.push_back(timer.id);
}
}
}
}
// Fire timers outside the lock to allow callbacks to create new timers
int fired_count = 0;
for (auto& timer : to_fire) {
FireTimer(timer);
fired_count++;
}
{
std::lock_guard<std::mutex> lock(m_mutex);
// Reschedule intervals
for (TimerId id : to_reschedule) {
auto it = std::find_if(m_timers.begin(), m_timers.end(),
[id](const Timer& t) { return t.id == id && !t.cancelled; });
if (it != m_timers.end()) {
RescheduleInterval(*it);
}
}
// Remove completed/cancelled timers
for (TimerId id : to_remove) {
auto it = std::find_if(m_timers.begin(), m_timers.end(),
[id](const Timer& t) { return t.id == id; });
if (it != m_timers.end()) {
// Release reference if not already released
if (it->callback_ref != LUA_NOREF && it->L && !it->is_interval) {
luaL_unref(it->L, LUA_REGISTRYINDEX, it->callback_ref);
}
// Update counts only for non-cancelled (timeout) timers
if (!it->cancelled && !it->is_interval) {
const std::string& app_id = it->app_id;
if (m_app_timer_counts[app_id] > 0) {
m_app_timer_counts[app_id]--;
}
m_app_timer_ids[app_id].erase(id);
}
m_timers.erase(it);
}
}
}
return fired_count;
}
size_t TimerManager::GetTimerCount(const std::string& app_id) const {
std::lock_guard<std::mutex> lock(m_mutex);
auto it = m_app_timer_counts.find(app_id);
if (it == m_app_timer_counts.end()) {
return 0;
}
return it->second;
}
//=============================================================================
// LUA API
//=============================================================================
// Registry keys for storing manager pointer and app_id
static const char* TIMER_MANAGER_KEY = "__mosis_timer_manager";
static const char* TIMER_APP_ID_KEY = "__mosis_timer_app_id";
// setTimeout(callback, delay_ms) -> timer_id
static int lua_setTimeout(lua_State* L) {
// Check arguments
luaL_checktype(L, 1, LUA_TFUNCTION);
int delay_ms = static_cast<int>(luaL_checkinteger(L, 2));
// Get timer manager from registry
lua_getfield(L, LUA_REGISTRYINDEX, TIMER_MANAGER_KEY);
if (!lua_islightuserdata(L, -1)) {
return luaL_error(L, "timer system not initialized");
}
TimerManager* manager = static_cast<TimerManager*>(lua_touserdata(L, -1));
lua_pop(L, 1);
// Get app_id from registry
lua_getfield(L, LUA_REGISTRYINDEX, TIMER_APP_ID_KEY);
if (!lua_isstring(L, -1)) {
return luaL_error(L, "app_id not set");
}
std::string app_id = lua_tostring(L, -1);
lua_pop(L, 1);
// Store the callback in registry
lua_pushvalue(L, 1); // Push the callback
int callback_ref = luaL_ref(L, LUA_REGISTRYINDEX);
// Create the timer
TimerId id = manager->SetTimeout(L, app_id, callback_ref, delay_ms);
if (id == 0) {
return luaL_error(L, "timer limit exceeded");
}
lua_pushinteger(L, static_cast<lua_Integer>(id));
return 1;
}
// clearTimeout(timer_id)
static int lua_clearTimeout(lua_State* L) {
TimerId id = static_cast<TimerId>(luaL_checkinteger(L, 1));
// Get timer manager from registry
lua_getfield(L, LUA_REGISTRYINDEX, TIMER_MANAGER_KEY);
if (!lua_islightuserdata(L, -1)) {
return luaL_error(L, "timer system not initialized");
}
TimerManager* manager = static_cast<TimerManager*>(lua_touserdata(L, -1));
lua_pop(L, 1);
// Get app_id from registry
lua_getfield(L, LUA_REGISTRYINDEX, TIMER_APP_ID_KEY);
if (!lua_isstring(L, -1)) {
return luaL_error(L, "app_id not set");
}
std::string app_id = lua_tostring(L, -1);
lua_pop(L, 1);
manager->ClearTimer(app_id, id);
return 0;
}
// setInterval(callback, interval_ms) -> timer_id
static int lua_setInterval(lua_State* L) {
// Check arguments
luaL_checktype(L, 1, LUA_TFUNCTION);
int interval_ms = static_cast<int>(luaL_checkinteger(L, 2));
// Get timer manager from registry
lua_getfield(L, LUA_REGISTRYINDEX, TIMER_MANAGER_KEY);
if (!lua_islightuserdata(L, -1)) {
return luaL_error(L, "timer system not initialized");
}
TimerManager* manager = static_cast<TimerManager*>(lua_touserdata(L, -1));
lua_pop(L, 1);
// Get app_id from registry
lua_getfield(L, LUA_REGISTRYINDEX, TIMER_APP_ID_KEY);
if (!lua_isstring(L, -1)) {
return luaL_error(L, "app_id not set");
}
std::string app_id = lua_tostring(L, -1);
lua_pop(L, 1);
// Store the callback in registry
lua_pushvalue(L, 1); // Push the callback
int callback_ref = luaL_ref(L, LUA_REGISTRYINDEX);
// Create the timer
TimerId id = manager->SetInterval(L, app_id, callback_ref, interval_ms);
if (id == 0) {
return luaL_error(L, "timer limit exceeded");
}
lua_pushinteger(L, static_cast<lua_Integer>(id));
return 1;
}
// clearInterval(timer_id)
static int lua_clearInterval(lua_State* L) {
// Same as clearTimeout
return lua_clearTimeout(L);
}
void RegisterTimerAPI(lua_State* L, TimerManager* manager, const std::string& app_id) {
// Store timer manager pointer in registry
lua_pushlightuserdata(L, manager);
lua_setfield(L, LUA_REGISTRYINDEX, TIMER_MANAGER_KEY);
// Store app_id in registry
lua_pushstring(L, app_id.c_str());
lua_setfield(L, LUA_REGISTRYINDEX, TIMER_APP_ID_KEY);
// Get the real _G (not the proxy)
// We need to set these in the real global table that the proxy reads from
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS);
// Check if we're dealing with a proxy (has __index metatable)
if (lua_getmetatable(L, -1)) {
lua_getfield(L, -1, "__index");
if (lua_istable(L, -1)) {
// We have a proxy, use the __index table as the real _G
lua_remove(L, -2); // Remove metatable
lua_remove(L, -2); // Remove proxy
// Now top of stack is real _G
lua_pushcfunction(L, lua_setTimeout);
lua_setfield(L, -2, "setTimeout");
lua_pushcfunction(L, lua_clearTimeout);
lua_setfield(L, -2, "clearTimeout");
lua_pushcfunction(L, lua_setInterval);
lua_setfield(L, -2, "setInterval");
lua_pushcfunction(L, lua_clearInterval);
lua_setfield(L, -2, "clearInterval");
lua_pop(L, 1); // Pop real _G
return;
}
lua_pop(L, 2); // Pop __index and metatable
}
// No proxy, just use _G directly
lua_pop(L, 1); // Pop whatever we got from LUA_RIDX_GLOBALS
// Register as globals
lua_pushcfunction(L, lua_setTimeout);
lua_setglobal(L, "setTimeout");
lua_pushcfunction(L, lua_clearTimeout);
lua_setglobal(L, "clearTimeout");
lua_pushcfunction(L, lua_setInterval);
lua_setglobal(L, "setInterval");
lua_pushcfunction(L, lua_clearInterval);
lua_setglobal(L, "clearInterval");
}
} // namespace mosis

View File

@@ -0,0 +1,706 @@
#include "virtual_fs.h"
#include <lua.hpp>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <algorithm>
#include <chrono>
namespace fs = std::filesystem;
namespace mosis {
//=============================================================================
// VIRTUALFS IMPLEMENTATION
//=============================================================================
VirtualFS::VirtualFS(const std::string& app_id,
const std::string& app_root,
const VirtualFSLimits& limits)
: m_app_id(app_id)
, m_app_root(app_root)
, m_limits(limits) {
// Ensure app root exists
std::error_code ec;
fs::create_directories(m_app_root, ec);
// Recalculate usage on startup
RecalculateUsage();
}
VirtualFS::~VirtualFS() {
}
//=============================================================================
// PATH VALIDATION
//=============================================================================
bool VirtualFS::IsValidPathChar(char c) {
// Allow alphanumeric, dash, underscore, dot, forward slash
return std::isalnum(static_cast<unsigned char>(c)) ||
c == '-' || c == '_' || c == '.' || c == '/';
}
int VirtualFS::GetPathDepth(const std::string& path) {
int depth = 0;
for (char c : path) {
if (c == '/') depth++;
}
return depth;
}
bool VirtualFS::ValidatePath(const std::string& virtual_path, std::string& error) {
// Check length
if (virtual_path.length() > m_limits.max_path_length) {
error = "path too long";
return false;
}
// Must start with /
if (virtual_path.empty() || virtual_path[0] != '/') {
error = "path must start with /";
return false;
}
// Check valid prefix
bool valid_prefix = false;
if (virtual_path.find("/data/") == 0 || virtual_path == "/data") {
valid_prefix = true;
} else if (virtual_path.find("/cache/") == 0 || virtual_path == "/cache") {
valid_prefix = true;
} else if (virtual_path.find("/temp/") == 0 || virtual_path == "/temp") {
valid_prefix = true;
} else if (virtual_path.find("/shared/") == 0 || virtual_path == "/shared") {
// Check permission for shared
if (CheckPermission && !CheckPermission("storage.shared")) {
error = "permission denied: storage.shared required";
return false;
}
valid_prefix = true;
}
if (!valid_prefix) {
error = "invalid path prefix (must be /data/, /cache/, /temp/, or /shared/)";
return false;
}
// Check for path traversal
if (virtual_path.find("..") != std::string::npos) {
error = "path traversal not allowed";
return false;
}
// Check for double slashes (except at start)
if (virtual_path.find("//") != std::string::npos) {
error = "invalid path (double slashes)";
return false;
}
// Check all characters are valid
for (char c : virtual_path) {
if (!IsValidPathChar(c)) {
error = "invalid character in path";
return false;
}
}
// Check depth
if (GetPathDepth(virtual_path) > m_limits.max_path_depth) {
error = "path too deep";
return false;
}
return true;
}
std::string VirtualFS::ResolvePath(const std::string& virtual_path) {
// Map virtual path to physical path
// /data/foo.txt -> <app_root>/data/foo.txt
// /cache/bar.txt -> <app_root>/cache/bar.txt
// /temp/baz.txt -> <app_root>/temp/baz.txt
// /shared/x.txt -> <app_root>/shared/x.txt
fs::path base(m_app_root);
// Remove leading slash and append
std::string relative = virtual_path.substr(1); // Remove leading /
return (base / relative).string();
}
//=============================================================================
// FILE OPERATIONS
//=============================================================================
bool VirtualFS::EnsureParentDir(const std::string& path) {
fs::path p(path);
fs::path parent = p.parent_path();
if (parent.empty()) return true;
std::error_code ec;
fs::create_directories(parent, ec);
return !ec;
}
void VirtualFS::UpdateUsage(int64_t delta) {
if (delta < 0 && static_cast<size_t>(-delta) > m_used_bytes) {
m_used_bytes = 0;
} else {
m_used_bytes = static_cast<size_t>(static_cast<int64_t>(m_used_bytes) + delta);
}
}
bool VirtualFS::CheckQuota(size_t additional_bytes, std::string& error) {
if (m_used_bytes + additional_bytes > m_limits.max_quota_bytes) {
error = "quota exceeded";
return false;
}
return true;
}
std::optional<std::string> VirtualFS::Read(const std::string& path, std::string& error) {
if (!ValidatePath(path, error)) {
return std::nullopt;
}
std::string physical_path = ResolvePath(path);
std::ifstream file(physical_path, std::ios::binary);
if (!file) {
error = "file not found";
return std::nullopt;
}
std::ostringstream ss;
ss << file.rdbuf();
return ss.str();
}
bool VirtualFS::Write(const std::string& path, const std::string& data, std::string& error) {
if (!ValidatePath(path, error)) {
return false;
}
// Check file size limit
if (data.size() > m_limits.max_file_size) {
error = "file size limit exceeded";
return false;
}
std::string physical_path = ResolvePath(path);
// Get current file size if exists (for quota calculation)
size_t old_size = 0;
std::error_code ec;
if (fs::exists(physical_path, ec)) {
old_size = static_cast<size_t>(fs::file_size(physical_path, ec));
}
// Check quota for net change
int64_t delta = static_cast<int64_t>(data.size()) - static_cast<int64_t>(old_size);
if (delta > 0 && !CheckQuota(static_cast<size_t>(delta), error)) {
return false;
}
// Ensure parent directory exists
if (!EnsureParentDir(physical_path)) {
error = "failed to create parent directory";
return false;
}
std::ofstream file(physical_path, std::ios::binary | std::ios::trunc);
if (!file) {
error = "failed to open file for writing";
return false;
}
file.write(data.data(), data.size());
if (!file) {
error = "failed to write data";
return false;
}
file.close();
UpdateUsage(delta);
return true;
}
bool VirtualFS::Append(const std::string& path, const std::string& data, std::string& error) {
if (!ValidatePath(path, error)) {
return false;
}
std::string physical_path = ResolvePath(path);
// Get current file size
size_t current_size = 0;
std::error_code ec;
if (fs::exists(physical_path, ec)) {
current_size = static_cast<size_t>(fs::file_size(physical_path, ec));
}
// Check file size limit
if (current_size + data.size() > m_limits.max_file_size) {
error = "file size limit exceeded";
return false;
}
// Check quota
if (!CheckQuota(data.size(), error)) {
return false;
}
// Ensure parent directory exists
if (!EnsureParentDir(physical_path)) {
error = "failed to create parent directory";
return false;
}
std::ofstream file(physical_path, std::ios::binary | std::ios::app);
if (!file) {
error = "failed to open file for appending";
return false;
}
file.write(data.data(), data.size());
if (!file) {
error = "failed to append data";
return false;
}
file.close();
UpdateUsage(static_cast<int64_t>(data.size()));
return true;
}
bool VirtualFS::Delete(const std::string& path, std::string& error) {
if (!ValidatePath(path, error)) {
return false;
}
std::string physical_path = ResolvePath(path);
std::error_code ec;
if (!fs::exists(physical_path, ec)) {
error = "file not found";
return false;
}
// Get size before deletion
size_t file_size = 0;
if (fs::is_regular_file(physical_path, ec)) {
file_size = static_cast<size_t>(fs::file_size(physical_path, ec));
}
if (!fs::remove(physical_path, ec)) {
error = "failed to delete";
return false;
}
UpdateUsage(-static_cast<int64_t>(file_size));
return true;
}
bool VirtualFS::Exists(const std::string& path) {
std::string error;
if (!ValidatePath(path, error)) {
return false;
}
std::string physical_path = ResolvePath(path);
std::error_code ec;
return fs::exists(physical_path, ec);
}
std::optional<std::vector<std::string>> VirtualFS::List(const std::string& path, std::string& error) {
if (!ValidatePath(path, error)) {
return std::nullopt;
}
std::string physical_path = ResolvePath(path);
std::error_code ec;
if (!fs::exists(physical_path, ec) || !fs::is_directory(physical_path, ec)) {
error = "directory not found";
return std::nullopt;
}
std::vector<std::string> entries;
for (const auto& entry : fs::directory_iterator(physical_path, ec)) {
entries.push_back(entry.path().filename().string());
}
if (ec) {
error = "failed to list directory";
return std::nullopt;
}
return entries;
}
bool VirtualFS::MakeDir(const std::string& path, std::string& error) {
if (!ValidatePath(path, error)) {
return false;
}
std::string physical_path = ResolvePath(path);
std::error_code ec;
if (!fs::create_directories(physical_path, ec) && ec) {
error = "failed to create directory";
return false;
}
return true;
}
std::optional<FileStat> VirtualFS::Stat(const std::string& path, std::string& error) {
if (!ValidatePath(path, error)) {
return std::nullopt;
}
std::string physical_path = ResolvePath(path);
std::error_code ec;
if (!fs::exists(physical_path, ec)) {
error = "file not found";
return std::nullopt;
}
FileStat stat;
stat.is_dir = fs::is_directory(physical_path, ec);
if (stat.is_dir) {
stat.size = 0;
} else {
stat.size = static_cast<size_t>(fs::file_size(physical_path, ec));
}
auto ftime = fs::last_write_time(physical_path, ec);
// Convert file_time_type to system_clock (portable workaround for clock_cast)
auto file_time_ns = ftime.time_since_epoch();
auto sys_time_ns = std::chrono::duration_cast<std::chrono::seconds>(file_time_ns);
stat.modified = sys_time_ns.count();
return stat;
}
//=============================================================================
// CLEANUP
//=============================================================================
void VirtualFS::DeleteDirectoryRecursive(const std::string& path) {
std::error_code ec;
fs::remove_all(path, ec);
}
size_t VirtualFS::CalculateDirectorySize(const std::string& path) {
size_t total = 0;
std::error_code ec;
if (!fs::exists(path, ec)) {
return 0;
}
for (const auto& entry : fs::recursive_directory_iterator(path, ec)) {
if (fs::is_regular_file(entry, ec)) {
total += static_cast<size_t>(fs::file_size(entry, ec));
}
}
return total;
}
void VirtualFS::RecalculateUsage() {
m_used_bytes = CalculateDirectorySize(m_app_root);
}
void VirtualFS::ClearTemp() {
fs::path temp_path = fs::path(m_app_root) / "temp";
std::error_code ec;
if (fs::exists(temp_path, ec)) {
size_t temp_size = CalculateDirectorySize(temp_path.string());
DeleteDirectoryRecursive(temp_path.string());
UpdateUsage(-static_cast<int64_t>(temp_size));
}
}
void VirtualFS::ClearAll() {
DeleteDirectoryRecursive(m_app_root);
m_used_bytes = 0;
}
//=============================================================================
// LUA API
//=============================================================================
static const char* VFS_KEY = "__mosis_vfs";
static VirtualFS* GetVFS(lua_State* L) {
lua_getfield(L, LUA_REGISTRYINDEX, VFS_KEY);
if (lua_islightuserdata(L, -1)) {
VirtualFS* vfs = static_cast<VirtualFS*>(lua_touserdata(L, -1));
lua_pop(L, 1);
return vfs;
}
lua_pop(L, 1);
return nullptr;
}
// fs.read(path) -> content|nil, error
static int lua_fs_read(lua_State* L) {
VirtualFS* vfs = GetVFS(L);
if (!vfs) {
lua_pushnil(L);
lua_pushstring(L, "VirtualFS not initialized");
return 2;
}
const char* path = luaL_checkstring(L, 1);
std::string error;
auto content = vfs->Read(path, error);
if (content) {
lua_pushlstring(L, content->data(), content->size());
return 1;
} else {
lua_pushnil(L);
lua_pushstring(L, error.c_str());
return 2;
}
}
// fs.write(path, data) -> bool, error
static int lua_fs_write(lua_State* L) {
VirtualFS* vfs = GetVFS(L);
if (!vfs) {
lua_pushboolean(L, 0);
lua_pushstring(L, "VirtualFS not initialized");
return 2;
}
const char* path = luaL_checkstring(L, 1);
size_t len;
const char* data = luaL_checklstring(L, 2, &len);
std::string error;
if (vfs->Write(path, std::string(data, len), error)) {
lua_pushboolean(L, 1);
return 1;
} else {
lua_pushboolean(L, 0);
lua_pushstring(L, error.c_str());
return 2;
}
}
// fs.append(path, data) -> bool, error
static int lua_fs_append(lua_State* L) {
VirtualFS* vfs = GetVFS(L);
if (!vfs) {
lua_pushboolean(L, 0);
lua_pushstring(L, "VirtualFS not initialized");
return 2;
}
const char* path = luaL_checkstring(L, 1);
size_t len;
const char* data = luaL_checklstring(L, 2, &len);
std::string error;
if (vfs->Append(path, std::string(data, len), error)) {
lua_pushboolean(L, 1);
return 1;
} else {
lua_pushboolean(L, 0);
lua_pushstring(L, error.c_str());
return 2;
}
}
// fs.delete(path) -> bool, error
static int lua_fs_delete(lua_State* L) {
VirtualFS* vfs = GetVFS(L);
if (!vfs) {
lua_pushboolean(L, 0);
lua_pushstring(L, "VirtualFS not initialized");
return 2;
}
const char* path = luaL_checkstring(L, 1);
std::string error;
if (vfs->Delete(path, error)) {
lua_pushboolean(L, 1);
return 1;
} else {
lua_pushboolean(L, 0);
lua_pushstring(L, error.c_str());
return 2;
}
}
// fs.exists(path) -> bool
static int lua_fs_exists(lua_State* L) {
VirtualFS* vfs = GetVFS(L);
if (!vfs) {
lua_pushboolean(L, 0);
return 1;
}
const char* path = luaL_checkstring(L, 1);
lua_pushboolean(L, vfs->Exists(path) ? 1 : 0);
return 1;
}
// fs.list(path) -> array|nil, error
static int lua_fs_list(lua_State* L) {
VirtualFS* vfs = GetVFS(L);
if (!vfs) {
lua_pushnil(L);
lua_pushstring(L, "VirtualFS not initialized");
return 2;
}
const char* path = luaL_checkstring(L, 1);
std::string error;
auto entries = vfs->List(path, error);
if (entries) {
lua_createtable(L, static_cast<int>(entries->size()), 0);
int i = 1;
for (const auto& name : *entries) {
lua_pushlstring(L, name.c_str(), name.size());
lua_rawseti(L, -2, i++);
}
return 1;
} else {
lua_pushnil(L);
lua_pushstring(L, error.c_str());
return 2;
}
}
// fs.mkdir(path) -> bool, error
static int lua_fs_mkdir(lua_State* L) {
VirtualFS* vfs = GetVFS(L);
if (!vfs) {
lua_pushboolean(L, 0);
lua_pushstring(L, "VirtualFS not initialized");
return 2;
}
const char* path = luaL_checkstring(L, 1);
std::string error;
if (vfs->MakeDir(path, error)) {
lua_pushboolean(L, 1);
return 1;
} else {
lua_pushboolean(L, 0);
lua_pushstring(L, error.c_str());
return 2;
}
}
// fs.stat(path) -> {size, modified, isDir}|nil, error
static int lua_fs_stat(lua_State* L) {
VirtualFS* vfs = GetVFS(L);
if (!vfs) {
lua_pushnil(L);
lua_pushstring(L, "VirtualFS not initialized");
return 2;
}
const char* path = luaL_checkstring(L, 1);
std::string error;
auto stat = vfs->Stat(path, error);
if (stat) {
lua_createtable(L, 0, 3);
lua_pushinteger(L, static_cast<lua_Integer>(stat->size));
lua_setfield(L, -2, "size");
lua_pushinteger(L, static_cast<lua_Integer>(stat->modified));
lua_setfield(L, -2, "modified");
lua_pushboolean(L, stat->is_dir ? 1 : 0);
lua_setfield(L, -2, "isDir");
return 1;
} else {
lua_pushnil(L);
lua_pushstring(L, error.c_str());
return 2;
}
}
// Helper to set a global in the real _G (bypassing any proxy)
static void SetGlobalInRealG(lua_State* L, const char* name) {
// Stack: value to set as global
// Get _G (might be a proxy)
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS);
// Check if it has a metatable with __index (proxy pattern)
if (lua_getmetatable(L, -1)) {
lua_getfield(L, -1, "__index");
if (lua_istable(L, -1)) {
// Found real _G through proxy's __index
// Stack: value, proxy, mt, real_G
lua_pushvalue(L, -4); // Copy value
lua_setfield(L, -2, name); // real_G[name] = value
lua_pop(L, 4); // pop real_G, mt, proxy, original value
return;
}
lua_pop(L, 2); // pop __index, metatable
}
// No proxy, set directly in _G
// Stack: value, _G
lua_pushvalue(L, -2); // Copy value
lua_setfield(L, -2, name); // _G[name] = value
lua_pop(L, 2); // pop _G, original value
}
void RegisterVirtualFS(lua_State* L, VirtualFS* vfs) {
// Store VFS in registry
lua_pushlightuserdata(L, vfs);
lua_setfield(L, LUA_REGISTRYINDEX, VFS_KEY);
// Create fs table
lua_newtable(L);
lua_pushcfunction(L, lua_fs_read);
lua_setfield(L, -2, "read");
lua_pushcfunction(L, lua_fs_write);
lua_setfield(L, -2, "write");
lua_pushcfunction(L, lua_fs_append);
lua_setfield(L, -2, "append");
lua_pushcfunction(L, lua_fs_delete);
lua_setfield(L, -2, "delete");
lua_pushcfunction(L, lua_fs_exists);
lua_setfield(L, -2, "exists");
lua_pushcfunction(L, lua_fs_list);
lua_setfield(L, -2, "list");
lua_pushcfunction(L, lua_fs_mkdir);
lua_setfield(L, -2, "mkdir");
lua_pushcfunction(L, lua_fs_stat);
lua_setfield(L, -2, "stat");
// Set as global (bypassing proxy)
SetGlobalInRealG(L, "fs");
}
} // namespace mosis

View File

@@ -30,7 +30,18 @@ FetchContent_MakeAvailable(rmlui)
# Get glad include directories explicitly # Get glad include directories explicitly
get_target_property(GLAD_INCLUDE_DIRS glad::glad INTERFACE_INCLUDE_DIRECTORIES) get_target_property(GLAD_INCLUDE_DIRS glad::glad INTERFACE_INCLUDE_DIRECTORIES)
# Shared kernel library (platform-agnostic code from MosisService) #==============================================================================
# Mosis Core Library (shared sandbox APIs)
#==============================================================================
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../core ${CMAKE_CURRENT_BINARY_DIR}/mosis-core)
# Point mosis-core to use the same Lua and JSON as designer
target_include_directories(mosis-core PUBLIC ${LUA_INCLUDE_DIR})
target_link_libraries(mosis-core PUBLIC ${LUA_LIBRARIES})
#==============================================================================
# Mosis Kernel (RmlUi renderer - platform specific for now)
#==============================================================================
add_library(mosis-kernel STATIC add_library(mosis-kernel STATIC
../src/main/cpp/RmlUi_Renderer_GL3.cpp ../src/main/cpp/RmlUi_Renderer_GL3.cpp
) )
@@ -55,15 +66,9 @@ target_compile_definitions(mosis-kernel PUBLIC
RMLUI_GL3_CUSTOM_LOADER="glad_loader.h" RMLUI_GL3_CUSTOM_LOADER="glad_loader.h"
) )
# Sandbox sources (reuse from MosisService) #==============================================================================
set(SANDBOX_SOURCES # Designer Executable
../src/main/cpp/sandbox/timer_manager.cpp #==============================================================================
../src/main/cpp/sandbox/json_api.cpp
../src/main/cpp/sandbox/crypto_api.cpp
../src/main/cpp/sandbox/virtual_fs.cpp
)
# Designer executable
add_executable(mosis-designer add_executable(mosis-designer
main.cpp main.cpp
src/desktop_platform.cpp src/desktop_platform.cpp
@@ -79,8 +84,6 @@ add_executable(mosis-designer
# Local backend with input recording hooks # Local backend with input recording hooks
src/backend/RmlUi_Backend_GLFW_GL3.cpp src/backend/RmlUi_Backend_GLFW_GL3.cpp
src/backend/RmlUi_Platform_GLFW.cpp src/backend/RmlUi_Platform_GLFW.cpp
# Sandbox APIs
${SANDBOX_SOURCES}
) )
target_include_directories(mosis-designer PRIVATE target_include_directories(mosis-designer PRIVATE
@@ -89,11 +92,14 @@ target_include_directories(mosis-designer PRIVATE
src/backend src/backend
../src/main/kernel/include ../src/main/kernel/include
../src/main/cpp ../src/main/cpp
../src/main/cpp/sandbox # Core library includes
../core/include
../core/include/mosis/sandbox
) )
target_link_libraries(mosis-designer PRIVATE target_link_libraries(mosis-designer PRIVATE
mosis-kernel mosis-kernel
mosis-core
glad::glad glad::glad
glfw glfw
freetype freetype