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:
23
core/include/mosis/apps/app_api.h
Normal file
23
core/include/mosis/apps/app_api.h
Normal 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
|
||||
167
core/include/mosis/apps/app_manager.h
Normal file
167
core/include/mosis/apps/app_manager.h
Normal 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
|
||||
57
core/include/mosis/platform/asset_interface.h
Normal file
57
core/include/mosis/platform/asset_interface.h
Normal 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
|
||||
98
core/include/mosis/platform/filesystem_interface.h
Normal file
98
core/include/mosis/platform/filesystem_interface.h
Normal 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
|
||||
94
core/include/mosis/sandbox/audit_log.h
Normal file
94
core/include/mosis/sandbox/audit_log.h
Normal 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;
|
||||
52
core/include/mosis/sandbox/crypto_api.h
Normal file
52
core/include/mosis/sandbox/crypto_api.h
Normal 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
|
||||
88
core/include/mosis/sandbox/database_manager.h
Normal file
88
core/include/mosis/sandbox/database_manager.h
Normal 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
|
||||
55
core/include/mosis/sandbox/http_validator.h
Normal file
55
core/include/mosis/sandbox/http_validator.h
Normal 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
|
||||
22
core/include/mosis/sandbox/json_api.h
Normal file
22
core/include/mosis/sandbox/json_api.h
Normal 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
|
||||
101
core/include/mosis/sandbox/lua_sandbox.h
Normal file
101
core/include/mosis/sandbox/lua_sandbox.h
Normal 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;
|
||||
76
core/include/mosis/sandbox/network_manager.h
Normal file
76
core/include/mosis/sandbox/network_manager.h
Normal 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
|
||||
52
core/include/mosis/sandbox/path_sandbox.h
Normal file
52
core/include/mosis/sandbox/path_sandbox.h
Normal 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
|
||||
73
core/include/mosis/sandbox/permission_gate.h
Normal file
73
core/include/mosis/sandbox/permission_gate.h
Normal 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;
|
||||
68
core/include/mosis/sandbox/rate_limiter.h
Normal file
68
core/include/mosis/sandbox/rate_limiter.h
Normal 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;
|
||||
87
core/include/mosis/sandbox/timer_manager.h
Normal file
87
core/include/mosis/sandbox/timer_manager.h
Normal 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
|
||||
77
core/include/mosis/sandbox/virtual_fs.h
Normal file
77
core/include/mosis/sandbox/virtual_fs.h
Normal 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
|
||||
40
core/include/mosis/util/logger.h
Normal file
40
core/include/mosis/util/logger.h
Normal 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__)
|
||||
Reference in New Issue
Block a user