implement Milestone 14: location interface with coarse/fine precision and rate limiting

This commit is contained in:
2026-01-18 16:07:32 +01:00
parent c2e8b8c212
commit 779f66b2bb
6 changed files with 1454 additions and 1 deletions

View File

@@ -0,0 +1,533 @@
// location_interface.cpp - Location interface implementation for Lua sandbox
// Milestone 14: Location access with privacy controls and precision levels
#include "location_interface.h"
#include <algorithm>
#include <cmath>
extern "C" {
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
}
namespace mosis {
// ============================================================================
// LocationWatch Implementation
// ============================================================================
LocationWatch::LocationWatch(int id, const LocationOptions& options)
: m_id(id)
, m_options(options)
{
}
LocationWatch::~LocationWatch() {
Stop();
}
void LocationWatch::Start() {
m_active = true;
}
void LocationWatch::Stop() {
m_active = false;
}
void LocationWatch::SimulatePosition(const Position& pos) {
if (m_active && m_on_position) {
m_on_position(pos);
}
}
void LocationWatch::SimulateError(LocationError error, const std::string& message) {
if (m_active && m_on_error) {
m_on_error(error, message);
}
}
// ============================================================================
// LocationInterface Implementation
// ============================================================================
LocationInterface::LocationInterface(const std::string& app_id)
: m_app_id(app_id)
{
}
LocationInterface::~LocationInterface() {
Shutdown();
}
Position LocationInterface::ApplyPrecision(const Position& pos) const {
if (m_has_fine_permission) {
return pos; // Full precision
}
// Coarse: round to ~1km (0.01 degrees ≈ 1.1km)
Position coarse = pos;
coarse.latitude = std::round(pos.latitude * 100.0) / 100.0;
coarse.longitude = std::round(pos.longitude * 100.0) / 100.0;
coarse.accuracy = std::max(pos.accuracy, COARSE_ACCURACY_M);
coarse.altitude = 0.0;
coarse.altitude_accuracy = 0.0;
coarse.heading = 0.0;
coarse.speed = 0.0;
return coarse;
}
bool LocationInterface::CheckRateLimit() {
auto now = std::chrono::steady_clock::now();
if (m_first_request) {
m_first_request = false;
m_last_request_time = now;
return true;
}
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
now - m_last_request_time
).count();
if (elapsed < RATE_LIMIT_MS) {
return false; // Rate limited
}
m_last_request_time = now;
return true;
}
bool LocationInterface::GetCurrentPosition(
const LocationOptions& options,
std::function<void(const Position&)> success,
std::function<void(LocationError, const std::string&)> error,
std::string& out_error
) {
// Check permission
if (!m_has_fine_permission && !m_has_coarse_permission) {
out_error = "Location permission denied";
return false;
}
// Check rate limit
if (!CheckRateLimit()) {
out_error = "Location request rate limited (1 per second)";
return false;
}
if (m_mock_mode) {
// In mock mode, immediately return the mock position
Position result = ApplyPrecision(m_mock_position);
result.timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()
).count();
if (success) {
success(result);
}
return true;
}
// Real implementation would use platform APIs here
out_error = "Real location not implemented";
return false;
}
std::shared_ptr<LocationWatch> LocationInterface::WatchPosition(
const LocationOptions& options,
std::string& error
) {
std::lock_guard<std::mutex> lock(m_mutex);
// Check permission
if (!m_has_fine_permission && !m_has_coarse_permission) {
error = "Location permission denied";
return nullptr;
}
// Clean up stopped watches first
CleanupStoppedWatches();
// Check watch limit
if (m_watches.size() >= MAX_WATCHES) {
error = "Maximum watch limit reached (5)";
return nullptr;
}
// Create watch
int id = m_next_watch_id++;
auto watch = std::make_shared<LocationWatch>(id, options);
watch->Start();
m_watches[id] = watch;
return watch;
}
void LocationInterface::ClearWatch(int watch_id) {
std::lock_guard<std::mutex> lock(m_mutex);
auto it = m_watches.find(watch_id);
if (it != m_watches.end()) {
it->second->Stop();
m_watches.erase(it);
}
}
void LocationInterface::ClearAllWatches() {
std::lock_guard<std::mutex> lock(m_mutex);
for (auto& [id, watch] : m_watches) {
watch->Stop();
}
m_watches.clear();
}
size_t LocationInterface::GetActiveWatchCount() const {
std::lock_guard<std::mutex> lock(m_mutex);
size_t count = 0;
for (const auto& [id, watch] : m_watches) {
if (watch->IsActive()) {
count++;
}
}
return count;
}
void LocationInterface::Shutdown() {
ClearAllWatches();
}
std::shared_ptr<LocationWatch> LocationInterface::GetWatch(int id) {
std::lock_guard<std::mutex> lock(m_mutex);
auto it = m_watches.find(id);
if (it != m_watches.end()) {
return it->second;
}
return nullptr;
}
void LocationInterface::CleanupStoppedWatches() {
// Called with lock held
for (auto it = m_watches.begin(); it != m_watches.end();) {
if (!it->second->IsActive()) {
it = m_watches.erase(it);
} else {
++it;
}
}
}
// ============================================================================
// Lua API Implementation
// ============================================================================
// Weak reference to watch stored in userdata
struct LuaWatchRef {
std::weak_ptr<LocationWatch> watch;
LocationInterface* location;
};
static const char* WATCH_METATABLE = "mosis.LocationWatch";
static LuaWatchRef* GetWatchRef(lua_State* L, int idx) {
return static_cast<LuaWatchRef*>(luaL_checkudata(L, idx, WATCH_METATABLE));
}
static std::shared_ptr<LocationWatch> GetWatch(lua_State* L, int idx) {
auto ref = GetWatchRef(L, idx);
return ref->watch.lock();
}
// Watch methods
static int LuaWatch_stop(lua_State* L) {
auto ref = GetWatchRef(L, 1);
auto watch = ref->watch.lock();
if (watch && ref->location) {
ref->location->ClearWatch(watch->GetId());
}
return 0;
}
static int LuaWatch_getId(lua_State* L) {
auto watch = GetWatch(L, 1);
if (watch) {
lua_pushinteger(L, watch->GetId());
} else {
lua_pushinteger(L, 0);
}
return 1;
}
static int LuaWatch_isActive(lua_State* L) {
auto watch = GetWatch(L, 1);
lua_pushboolean(L, watch && watch->IsActive());
return 1;
}
static int LuaWatch_gc(lua_State* L) {
auto ref = GetWatchRef(L, 1);
ref->~LuaWatchRef();
return 0;
}
static const luaL_Reg watch_methods[] = {
{"stop", LuaWatch_stop},
{"getId", LuaWatch_getId},
{"isActive", LuaWatch_isActive},
{nullptr, nullptr}
};
static void CreateWatchMetatable(lua_State* L) {
luaL_newmetatable(L, WATCH_METATABLE);
// __index = methods table
lua_newtable(L);
luaL_setfuncs(L, watch_methods, 0);
lua_setfield(L, -2, "__index");
// __gc
lua_pushcfunction(L, LuaWatch_gc);
lua_setfield(L, -2, "__gc");
lua_pop(L, 1);
}
static void PushWatch(lua_State* L, std::shared_ptr<LocationWatch> watch, LocationInterface* location) {
auto ref = static_cast<LuaWatchRef*>(lua_newuserdata(L, sizeof(LuaWatchRef)));
new (ref) LuaWatchRef{watch, location};
luaL_setmetatable(L, WATCH_METATABLE);
}
static const char* LocationErrorToString(LocationError err) {
switch (err) {
case LocationError::PermissionDenied: return "PERMISSION_DENIED";
case LocationError::PositionUnavailable: return "POSITION_UNAVAILABLE";
case LocationError::Timeout: return "TIMEOUT";
case LocationError::RateLimited: return "RATE_LIMITED";
case LocationError::WatchLimitReached: return "WATCH_LIMIT_REACHED";
default: return "UNKNOWN";
}
}
// location.getCurrentPosition(successCallback, errorCallback, options)
static int LuaLocation_getCurrentPosition(lua_State* L) {
auto location = static_cast<LocationInterface*>(lua_touserdata(L, lua_upvalueindex(1)));
if (!location) {
lua_pushnil(L);
lua_pushstring(L, "Location interface not available");
return 2;
}
// Validate callbacks
if (!lua_isfunction(L, 1)) {
return luaL_error(L, "First argument must be a success callback function");
}
if (!lua_isfunction(L, 2) && !lua_isnil(L, 2)) {
return luaL_error(L, "Second argument must be an error callback function or nil");
}
// Parse options from third argument (optional table)
LocationOptions options;
if (lua_istable(L, 3)) {
lua_getfield(L, 3, "enableHighAccuracy");
if (lua_isboolean(L, -1)) {
options.enable_high_accuracy = lua_toboolean(L, -1);
}
lua_pop(L, 1);
lua_getfield(L, 3, "timeout");
if (lua_isnumber(L, -1)) {
options.timeout_ms = static_cast<int>(lua_tointeger(L, -1));
}
lua_pop(L, 1);
lua_getfield(L, 3, "maxAge");
if (lua_isnumber(L, -1)) {
options.max_age_ms = static_cast<int>(lua_tointeger(L, -1));
}
lua_pop(L, 1);
}
// Store callbacks in registry for later use
lua_pushvalue(L, 1); // success callback
int success_ref = luaL_ref(L, LUA_REGISTRYINDEX);
int error_ref = LUA_NOREF;
if (lua_isfunction(L, 2)) {
lua_pushvalue(L, 2); // error callback
error_ref = luaL_ref(L, LUA_REGISTRYINDEX);
}
std::string err;
bool ok = location->GetCurrentPosition(
options,
[L, success_ref](const Position& pos) {
// Call success callback
lua_rawgeti(L, LUA_REGISTRYINDEX, success_ref);
if (lua_isfunction(L, -1)) {
// Create position table
lua_newtable(L);
lua_pushnumber(L, pos.latitude);
lua_setfield(L, -2, "latitude");
lua_pushnumber(L, pos.longitude);
lua_setfield(L, -2, "longitude");
lua_pushnumber(L, pos.accuracy);
lua_setfield(L, -2, "accuracy");
lua_pushnumber(L, pos.altitude);
lua_setfield(L, -2, "altitude");
lua_pushnumber(L, pos.altitude_accuracy);
lua_setfield(L, -2, "altitudeAccuracy");
lua_pushnumber(L, pos.heading);
lua_setfield(L, -2, "heading");
lua_pushnumber(L, pos.speed);
lua_setfield(L, -2, "speed");
lua_pushinteger(L, pos.timestamp);
lua_setfield(L, -2, "timestamp");
lua_pcall(L, 1, 0, 0);
} else {
lua_pop(L, 1);
}
luaL_unref(L, LUA_REGISTRYINDEX, success_ref);
},
[L, error_ref](LocationError error, const std::string& message) {
if (error_ref != LUA_NOREF) {
lua_rawgeti(L, LUA_REGISTRYINDEX, error_ref);
if (lua_isfunction(L, -1)) {
lua_pushstring(L, LocationErrorToString(error));
lua_pushstring(L, message.c_str());
lua_pcall(L, 2, 0, 0);
} else {
lua_pop(L, 1);
}
luaL_unref(L, LUA_REGISTRYINDEX, error_ref);
}
},
err
);
if (!ok) {
// Clean up refs on failure
luaL_unref(L, LUA_REGISTRYINDEX, success_ref);
if (error_ref != LUA_NOREF) {
luaL_unref(L, LUA_REGISTRYINDEX, error_ref);
}
lua_pushnil(L);
lua_pushstring(L, err.c_str());
return 2;
}
lua_pushboolean(L, 1);
return 1;
}
// location.watchPosition(successCallback, errorCallback, options)
static int LuaLocation_watchPosition(lua_State* L) {
auto location = static_cast<LocationInterface*>(lua_touserdata(L, lua_upvalueindex(1)));
if (!location) {
lua_pushnil(L);
lua_pushstring(L, "Location interface not available");
return 2;
}
// Parse options from third argument (optional table)
LocationOptions options;
if (lua_istable(L, 3)) {
lua_getfield(L, 3, "enableHighAccuracy");
if (lua_isboolean(L, -1)) {
options.enable_high_accuracy = lua_toboolean(L, -1);
}
lua_pop(L, 1);
lua_getfield(L, 3, "timeout");
if (lua_isnumber(L, -1)) {
options.timeout_ms = static_cast<int>(lua_tointeger(L, -1));
}
lua_pop(L, 1);
}
std::string err;
auto watch = location->WatchPosition(options, err);
if (!watch) {
lua_pushnil(L);
lua_pushstring(L, err.c_str());
return 2;
}
PushWatch(L, watch, location);
return 1;
}
// location.clearWatch(watchId)
static int LuaLocation_clearWatch(lua_State* L) {
auto location = static_cast<LocationInterface*>(lua_touserdata(L, lua_upvalueindex(1)));
if (location) {
int watch_id = static_cast<int>(luaL_checkinteger(L, 1));
location->ClearWatch(watch_id);
}
return 0;
}
// location.getWatchCount()
static int LuaLocation_getWatchCount(lua_State* L) {
auto location = static_cast<LocationInterface*>(lua_touserdata(L, lua_upvalueindex(1)));
if (location) {
lua_pushinteger(L, static_cast<lua_Integer>(location->GetActiveWatchCount()));
} else {
lua_pushinteger(L, 0);
}
return 1;
}
// Helper to set a global in the real _G (bypassing any proxy)
static void SetGlobalInRealG(lua_State* L, const char* name) {
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS);
if (lua_getmetatable(L, -1)) {
lua_getfield(L, -1, "__index");
if (lua_istable(L, -1)) {
lua_pushvalue(L, -4);
lua_setfield(L, -2, name);
lua_pop(L, 4);
return;
}
lua_pop(L, 2);
}
lua_pushvalue(L, -2);
lua_setfield(L, -2, name);
lua_pop(L, 2);
}
void RegisterLocationAPI(lua_State* L, LocationInterface* location) {
// Create watch metatable
CreateWatchMetatable(L);
// Create location table
lua_newtable(L);
// location.getCurrentPosition
lua_pushlightuserdata(L, location);
lua_pushcclosure(L, LuaLocation_getCurrentPosition, 1);
lua_setfield(L, -2, "getCurrentPosition");
// location.watchPosition
lua_pushlightuserdata(L, location);
lua_pushcclosure(L, LuaLocation_watchPosition, 1);
lua_setfield(L, -2, "watchPosition");
// location.clearWatch
lua_pushlightuserdata(L, location);
lua_pushcclosure(L, LuaLocation_clearWatch, 1);
lua_setfield(L, -2, "clearWatch");
// location.getWatchCount
lua_pushlightuserdata(L, location);
lua_pushcclosure(L, LuaLocation_getWatchCount, 1);
lua_setfield(L, -2, "getWatchCount");
// Set as global (bypasses sandbox proxy)
SetGlobalInRealG(L, "location");
}
} // namespace mosis

View File

@@ -0,0 +1,152 @@
// location_interface.h - Location interface for Lua sandbox
// Milestone 14: Location access with privacy controls and precision levels
#pragma once
#include <string>
#include <memory>
#include <functional>
#include <mutex>
#include <atomic>
#include <vector>
#include <unordered_map>
#include <chrono>
struct lua_State;
namespace mosis {
struct Position {
double latitude = 0.0;
double longitude = 0.0;
double accuracy = 0.0; // meters
double altitude = 0.0; // meters (optional)
double altitude_accuracy = 0.0; // meters (optional)
double heading = 0.0; // degrees (optional)
double speed = 0.0; // m/s (optional)
int64_t timestamp = 0; // milliseconds since epoch
};
struct LocationOptions {
bool enable_high_accuracy = false;
int timeout_ms = 30000; // max time to wait
int max_age_ms = 0; // accept cached position this old
};
enum class LocationError {
None,
PermissionDenied,
PositionUnavailable,
Timeout,
RateLimited,
WatchLimitReached
};
class LocationWatch {
public:
using PositionCallback = std::function<void(const Position&)>;
using ErrorCallback = std::function<void(LocationError, const std::string&)>;
LocationWatch(int id, const LocationOptions& options);
~LocationWatch();
int GetId() const { return m_id; }
bool IsActive() const { return m_active; }
void Start();
void Stop();
void SetOnPosition(PositionCallback cb) { m_on_position = std::move(cb); }
void SetOnError(ErrorCallback cb) { m_on_error = std::move(cb); }
// For mock mode - simulate position update
void SimulatePosition(const Position& pos);
void SimulateError(LocationError error, const std::string& message);
private:
int m_id;
LocationOptions m_options;
std::atomic<bool> m_active{false};
PositionCallback m_on_position;
ErrorCallback m_on_error;
mutable std::mutex m_mutex;
};
class LocationInterface {
public:
LocationInterface(const std::string& app_id);
~LocationInterface();
// Check if app has location permission
bool HasFinePermission() const { return m_has_fine_permission; }
bool HasCoarsePermission() const { return m_has_coarse_permission; }
void SetFinePermission(bool granted) { m_has_fine_permission = granted; }
void SetCoarsePermission(bool granted) { m_has_coarse_permission = granted; }
// Get current position (one-shot)
// Returns true if request started, false on error
bool GetCurrentPosition(
const LocationOptions& options,
std::function<void(const Position&)> success,
std::function<void(LocationError, const std::string&)> error,
std::string& out_error
);
// Watch position (continuous)
// Returns watch on success, nullptr on failure
std::shared_ptr<LocationWatch> WatchPosition(
const LocationOptions& options,
std::string& error
);
// Stop a specific watch
void ClearWatch(int watch_id);
// Stop all watches for this app
void ClearAllWatches();
// Get active watch count
size_t GetActiveWatchCount() const;
// Cleanup on app stop
void Shutdown();
// For testing
void SetMockMode(bool enabled) { m_mock_mode = enabled; }
bool IsMockMode() const { return m_mock_mode; }
// Mock mode: set the position to return
void SetMockPosition(const Position& pos) { m_mock_position = pos; }
// Get watch by ID
std::shared_ptr<LocationWatch> GetWatch(int id);
// For rate limiting - check and consume
bool CheckRateLimit();
private:
std::string m_app_id;
std::unordered_map<int, std::shared_ptr<LocationWatch>> m_watches;
mutable std::mutex m_mutex;
bool m_mock_mode = true;
bool m_has_fine_permission = false;
bool m_has_coarse_permission = false;
Position m_mock_position;
int m_next_watch_id = 1;
std::chrono::steady_clock::time_point m_last_request_time;
bool m_first_request = true;
static constexpr int MAX_WATCHES = 5;
static constexpr int RATE_LIMIT_MS = 1000; // 1 request per second
static constexpr double COARSE_ACCURACY_M = 1000.0; // 1km for coarse
// Apply precision reduction for coarse permission
Position ApplyPrecision(const Position& pos) const;
void CleanupStoppedWatches();
};
// Register location.* APIs as globals
void RegisterLocationAPI(lua_State* L, LocationInterface* location);
} // namespace mosis