mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-29 20:01:35 +00:00
453 lines
12 KiB
Lua
453 lines
12 KiB
Lua
local APP_ID_MAX_LENGTH = 64
|
|
local DESCRIPTION_MAX_LENGTH = 320
|
|
local DEVELOPER_MAX_LENGTH = 96
|
|
local NAME_MAX_LENGTH = 64
|
|
local PATH_MAX_LENGTH = 512
|
|
local VERSION_MAX_LENGTH = 32
|
|
|
|
local ALLOWED_CATEGORIES = {
|
|
games = true,
|
|
productivity = true,
|
|
shopping = true,
|
|
social = true,
|
|
utilities = true,
|
|
}
|
|
|
|
local ALLOWED_ORIENTATIONS = {
|
|
any = true,
|
|
landscape = true,
|
|
portrait = true,
|
|
}
|
|
|
|
local ALLOWED_PERMISSIONS = {
|
|
["app.close"] = true,
|
|
["app.open"] = true,
|
|
["camera.capture"] = true,
|
|
["contacts.pick"] = true,
|
|
["device.storage"] = true,
|
|
["locale.read"] = true,
|
|
["location.read"] = true,
|
|
["media.pick"] = true,
|
|
["notifications"] = true,
|
|
["notifications.critical"] = true,
|
|
["nui.fetch"] = true,
|
|
["theme.read"] = true,
|
|
}
|
|
|
|
local RESERVED_APP_IDS = {
|
|
["app-store"] = true,
|
|
banking = true,
|
|
billing = true,
|
|
calculator = true,
|
|
calendar = true,
|
|
camera = true,
|
|
citymarkt = true,
|
|
clock = true,
|
|
companies = true,
|
|
crewlink = true,
|
|
darkchat = true,
|
|
feather = true,
|
|
flare = true,
|
|
fliptok = true,
|
|
garage = true,
|
|
house = true,
|
|
["local-pages"] = true,
|
|
mail = true,
|
|
map = true,
|
|
memory = true,
|
|
messages = true,
|
|
minesweeper = true,
|
|
music = true,
|
|
["neon-drop"] = true,
|
|
notes = true,
|
|
memos = true,
|
|
["number-merge"] = true,
|
|
phone = true,
|
|
photos = true,
|
|
picstagram = true,
|
|
radio = true,
|
|
settings = true,
|
|
["sky-flappy"] = true,
|
|
skyride = true,
|
|
snake = true,
|
|
["tower-stack"] = true,
|
|
weather = true,
|
|
}
|
|
|
|
local bundled_manifests = {}
|
|
local bundled_manifests_by_id = {}
|
|
|
|
SkyPhoneApps = SkyPhoneApps or {}
|
|
SkyPhoneApps.ProtocolVersion = 1
|
|
|
|
local function trim(value)
|
|
return value:match("^%s*(.-)%s*$")
|
|
end
|
|
|
|
local function validate_app_id(app_id)
|
|
if type(app_id) ~= "string" then
|
|
return false, "invalid_app_id"
|
|
end
|
|
|
|
if #app_id == 0 or #app_id > APP_ID_MAX_LENGTH then
|
|
return false, "invalid_app_id"
|
|
end
|
|
|
|
if not app_id:match("^[a-z0-9][a-z0-9._-]+$") then
|
|
return false, "invalid_app_id"
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
local function validate_text(value, error_code, maximum_length, required)
|
|
if value == nil and not required then
|
|
return nil
|
|
end
|
|
|
|
if type(value) ~= "string" then
|
|
return nil, error_code
|
|
end
|
|
|
|
local normalized = trim(value)
|
|
if (required and #normalized == 0) or #normalized > maximum_length then
|
|
return nil, error_code
|
|
end
|
|
|
|
return normalized
|
|
end
|
|
|
|
local function validate_localized_text(value, error_code, maximum_length, required)
|
|
if type(value) == "string" then
|
|
return validate_text(value, error_code, maximum_length, required)
|
|
end
|
|
|
|
if type(value) ~= "table" then
|
|
return nil, error_code
|
|
end
|
|
|
|
local normalized = {}
|
|
local count = 0
|
|
for locale, text in pairs(value) do
|
|
if type(locale) ~= "string" or not locale:match("^[a-zA-Z][a-zA-Z0-9_-]*$") then
|
|
return nil, error_code
|
|
end
|
|
|
|
local normalized_text = validate_text(text, error_code, maximum_length, true)
|
|
if not normalized_text then
|
|
return nil, error_code
|
|
end
|
|
|
|
count = count + 1
|
|
if count > 16 then
|
|
return nil, error_code
|
|
end
|
|
|
|
normalized[locale:lower():gsub("_", "-")] = normalized_text
|
|
end
|
|
|
|
if count == 0 then
|
|
return nil, error_code
|
|
end
|
|
|
|
return normalized
|
|
end
|
|
|
|
local function validate_local_path(path, required)
|
|
if path == nil and not required then
|
|
return nil
|
|
end
|
|
|
|
if type(path) ~= "string" or #path == 0 or #path > PATH_MAX_LENGTH then
|
|
return nil, "invalid_local_path"
|
|
end
|
|
|
|
if path:sub(1, 1) == "/" or path:find("\\", 1, true) or path:find(":", 1, true) then
|
|
return nil, "invalid_local_path"
|
|
end
|
|
|
|
if not path:match("^web/[%w%._%-%/]+$") then
|
|
return nil, "invalid_local_path"
|
|
end
|
|
|
|
for segment in path:gmatch("[^/]+") do
|
|
if segment == "." or segment == ".." then
|
|
return nil, "invalid_local_path"
|
|
end
|
|
end
|
|
|
|
return path
|
|
end
|
|
|
|
local function validate_string_array(value, error_code, maximum_items, validator)
|
|
if value == nil then
|
|
return {}
|
|
end
|
|
|
|
if type(value) ~= "table" then
|
|
return nil, error_code
|
|
end
|
|
|
|
local normalized = {}
|
|
local count = 0
|
|
for key in pairs(value) do
|
|
if type(key) ~= "number" or key < 1 or key % 1 ~= 0 then
|
|
return nil, error_code
|
|
end
|
|
count = count + 1
|
|
end
|
|
|
|
if count ~= #value or count > maximum_items then
|
|
return nil, error_code
|
|
end
|
|
|
|
for index = 1, count do
|
|
local item = value[index]
|
|
if type(item) ~= "string" then
|
|
return nil, error_code
|
|
end
|
|
|
|
local normalized_item, validation_error = validator(item)
|
|
if not normalized_item then
|
|
return nil, validation_error or error_code
|
|
end
|
|
|
|
normalized[index] = normalized_item
|
|
end
|
|
|
|
return normalized
|
|
end
|
|
|
|
local function validate_permissions(value)
|
|
local permissions, error_code = validate_string_array(
|
|
value,
|
|
"invalid_permissions",
|
|
32,
|
|
function(permission)
|
|
if not ALLOWED_PERMISSIONS[permission] then
|
|
return nil, "unknown_permission"
|
|
end
|
|
return permission
|
|
end
|
|
)
|
|
if not permissions then
|
|
return nil, error_code
|
|
end
|
|
|
|
local seen = {}
|
|
for index = 1, #permissions do
|
|
local permission = permissions[index]
|
|
if seen[permission] then
|
|
return nil, "duplicate_permission"
|
|
end
|
|
seen[permission] = true
|
|
end
|
|
|
|
return permissions
|
|
end
|
|
|
|
local function validate_grid_order(value)
|
|
if value == nil then
|
|
return nil
|
|
end
|
|
|
|
if type(value) ~= "number" or value ~= math.floor(value) or value < 0 or value > 9999 then
|
|
return nil, "invalid_grid_order"
|
|
end
|
|
|
|
return value
|
|
end
|
|
|
|
local function validate_icon_background(value)
|
|
if value == nil then
|
|
return nil
|
|
end
|
|
|
|
if type(value) ~= "string" or #value == 0 or #value > 64 then
|
|
return nil, "invalid_icon_background"
|
|
end
|
|
|
|
if value:find("[\r\n;{}]") then
|
|
return nil, "invalid_icon_background"
|
|
end
|
|
|
|
return value
|
|
end
|
|
|
|
local function normalize_manifest(folder, definition)
|
|
if type(folder) ~= "string" or type(definition) ~= "table" then
|
|
return nil, "invalid_manifest"
|
|
end
|
|
|
|
if definition.schemaVersion ~= SkyPhoneApps.ProtocolVersion then
|
|
return nil, "unsupported_schema_version"
|
|
end
|
|
|
|
local app_id = definition.id
|
|
local valid_id, id_error = validate_app_id(app_id)
|
|
if not valid_id then
|
|
return nil, id_error
|
|
end
|
|
if RESERVED_APP_IDS[app_id] then
|
|
return nil, "reserved_app_id"
|
|
end
|
|
if folder ~= app_id then
|
|
return nil, "folder_id_mismatch"
|
|
end
|
|
|
|
if definition.defaultInstalled ~= nil and type(definition.defaultInstalled) ~= "boolean" then
|
|
return nil, "invalid_default_installed"
|
|
end
|
|
if definition.removable ~= nil and type(definition.removable) ~= "boolean" then
|
|
return nil, "invalid_removable"
|
|
end
|
|
|
|
local version, version_error = validate_text(
|
|
definition.version or "1.0.0",
|
|
"invalid_version",
|
|
VERSION_MAX_LENGTH,
|
|
true
|
|
)
|
|
if not version then
|
|
return nil, version_error
|
|
end
|
|
if not version:match("^[%w%._+-]+$") then
|
|
return nil, "invalid_version"
|
|
end
|
|
|
|
local name, name_error = validate_localized_text(
|
|
definition.name,
|
|
"invalid_name",
|
|
NAME_MAX_LENGTH,
|
|
true
|
|
)
|
|
if not name then
|
|
return nil, name_error
|
|
end
|
|
|
|
local description, description_error = validate_localized_text(
|
|
definition.description,
|
|
"invalid_description",
|
|
DESCRIPTION_MAX_LENGTH,
|
|
true
|
|
)
|
|
if not description then
|
|
return nil, description_error
|
|
end
|
|
|
|
local developer, developer_error = validate_text(
|
|
definition.developer,
|
|
"invalid_developer",
|
|
DEVELOPER_MAX_LENGTH,
|
|
false
|
|
)
|
|
if definition.developer ~= nil and not developer then
|
|
return nil, developer_error
|
|
end
|
|
|
|
local category = definition.category or "utilities"
|
|
if not ALLOWED_CATEGORIES[category] then
|
|
return nil, "invalid_category"
|
|
end
|
|
|
|
local entry, entry_error = validate_local_path(definition.entry, true)
|
|
if not entry then
|
|
return nil, entry_error
|
|
end
|
|
|
|
local icon, icon_error = validate_local_path(definition.icon, false)
|
|
if definition.icon ~= nil and not icon then
|
|
return nil, icon_error
|
|
end
|
|
|
|
local screenshots, screenshots_error = validate_string_array(
|
|
definition.screenshots,
|
|
"invalid_screenshots",
|
|
8,
|
|
function(path)
|
|
return validate_local_path(path, true)
|
|
end
|
|
)
|
|
if not screenshots then
|
|
return nil, screenshots_error
|
|
end
|
|
|
|
local permissions, permissions_error = validate_permissions(definition.permissions)
|
|
if not permissions then
|
|
return nil, permissions_error
|
|
end
|
|
|
|
local orientation = definition.orientation or "portrait"
|
|
if not ALLOWED_ORIENTATIONS[orientation] then
|
|
return nil, "invalid_orientation"
|
|
end
|
|
|
|
local grid_order, grid_order_error = validate_grid_order(definition.gridOrder)
|
|
if definition.gridOrder ~= nil and not grid_order then
|
|
return nil, grid_order_error
|
|
end
|
|
|
|
local icon_background, background_error = validate_icon_background(definition.iconBackground)
|
|
if definition.iconBackground ~= nil and not icon_background then
|
|
return nil, background_error
|
|
end
|
|
|
|
local bridge_mode = definition.bridgeMode or "sky"
|
|
if bridge_mode ~= "sky" and bridge_mode ~= "legacy" then
|
|
return nil, "invalid_bridge_mode"
|
|
end
|
|
|
|
return {
|
|
bridgeMode = bridge_mode,
|
|
category = category,
|
|
defaultInstalled = definition.defaultInstalled == true,
|
|
description = description,
|
|
developer = developer,
|
|
entry = entry,
|
|
folder = folder,
|
|
gridOrder = grid_order,
|
|
icon = icon,
|
|
iconBackground = icon_background,
|
|
id = app_id,
|
|
name = name,
|
|
orientation = orientation,
|
|
permissions = permissions,
|
|
removable = definition.removable ~= false,
|
|
schemaVersion = SkyPhoneApps.ProtocolVersion,
|
|
screenshots = screenshots,
|
|
version = version,
|
|
}
|
|
end
|
|
|
|
---@param folder string App folder relative to custom_apps.
|
|
---@param definition table Declarative bundled app manifest.
|
|
function SkyPhoneApps.RegisterManifest(folder, definition)
|
|
local manifest, error_code = normalize_manifest(folder, definition)
|
|
if not manifest then
|
|
error(("[sky_phone] Invalid custom app manifest '%s': %s"):format(tostring(folder), error_code), 2)
|
|
end
|
|
|
|
if bundled_manifests_by_id[manifest.id] then
|
|
error(("[sky_phone] Duplicate bundled custom app id '%s'."):format(manifest.id), 2)
|
|
end
|
|
|
|
bundled_manifests[#bundled_manifests + 1] = manifest
|
|
bundled_manifests_by_id[manifest.id] = manifest
|
|
end
|
|
|
|
function SkyPhoneApps.GetBundledManifests()
|
|
return bundled_manifests
|
|
end
|
|
|
|
function SkyPhoneApps.GetBundledManifest(app_id)
|
|
return bundled_manifests_by_id[app_id]
|
|
end
|
|
|
|
SkyPhoneApps.AllowedCategories = ALLOWED_CATEGORIES
|
|
SkyPhoneApps.AllowedOrientations = ALLOWED_ORIENTATIONS
|
|
SkyPhoneApps.AllowedPermissions = ALLOWED_PERMISSIONS
|
|
SkyPhoneApps.ReservedAppIds = RESERVED_APP_IDS
|
|
SkyPhoneApps.ValidateAppId = validate_app_id
|
|
SkyPhoneApps.ValidateLocalizedText = validate_localized_text
|
|
SkyPhoneApps.ValidatePermissions = validate_permissions
|