Files
MosisService/portal/pkg/mospkg/manifest.go

186 lines
5.2 KiB
Go

// Package mospkg provides functionality for Mosis app packages (.mosis files)
package mospkg
import (
"encoding/json"
"fmt"
"regexp"
)
// Manifest represents the app manifest (manifest.json)
type Manifest struct {
Schema string `json:"$schema,omitempty"`
ID string `json:"id"`
Name string `json:"name"`
Version string `json:"version"`
VersionCode int `json:"version_code"`
Entry string `json:"entry"`
MinMosisVersion string `json:"min_mosis_version"`
Description string `json:"description,omitempty"`
Author *Author `json:"author,omitempty"`
Permissions []string `json:"permissions,omitempty"`
Icons Icons `json:"icons,omitempty"`
TargetMosisVer string `json:"target_mosis_version,omitempty"`
Category string `json:"category,omitempty"`
Tags []string `json:"tags,omitempty"`
Orientation string `json:"orientation,omitempty"`
BackgroundColor string `json:"background_color,omitempty"`
Locales []string `json:"locales,omitempty"`
DefaultLocale string `json:"default_locale,omitempty"`
}
// Author represents the app author information
type Author struct {
Name string `json:"name"`
Email string `json:"email,omitempty"`
URL string `json:"url,omitempty"`
}
// Icons represents app icon paths by size
type Icons struct {
Size32 string `json:"32,omitempty"`
Size64 string `json:"64,omitempty"`
Size128 string `json:"128,omitempty"`
}
// ParseManifest parses a manifest.json from bytes
func ParseManifest(data []byte) (*Manifest, error) {
var m Manifest
if err := json.Unmarshal(data, &m); err != nil {
return nil, fmt.Errorf("parse manifest: %w", err)
}
return &m, nil
}
// Validate checks if the manifest contains all required fields with valid values
func (m *Manifest) Validate() []ValidationError {
var errors []ValidationError
// Required fields
if m.ID == "" {
errors = append(errors, ValidationError{
Code: "MISSING_ID",
Message: "Manifest is missing required field: id",
})
} else if !isValidPackageID(m.ID) {
errors = append(errors, ValidationError{
Code: "INVALID_ID",
Message: "Package ID must be in reverse domain format (e.g., com.developer.app)",
File: "manifest.json",
})
}
if m.Name == "" {
errors = append(errors, ValidationError{
Code: "MISSING_NAME",
Message: "Manifest is missing required field: name",
})
} else if len(m.Name) > 30 {
errors = append(errors, ValidationError{
Code: "NAME_TOO_LONG",
Message: "App name must be 30 characters or less",
})
}
if m.Version == "" {
errors = append(errors, ValidationError{
Code: "MISSING_VERSION",
Message: "Manifest is missing required field: version",
})
} else if !isValidSemver(m.Version) {
errors = append(errors, ValidationError{
Code: "INVALID_VERSION",
Message: "Version must be in semantic version format (X.Y.Z)",
})
}
if m.VersionCode <= 0 {
errors = append(errors, ValidationError{
Code: "INVALID_VERSION_CODE",
Message: "version_code must be a positive integer",
})
}
if m.Entry == "" {
errors = append(errors, ValidationError{
Code: "MISSING_ENTRY",
Message: "Manifest is missing required field: entry",
})
}
if m.MinMosisVersion == "" {
errors = append(errors, ValidationError{
Code: "MISSING_MIN_MOSIS_VERSION",
Message: "Manifest is missing required field: min_mosis_version",
})
}
// Validate permissions
for _, perm := range m.Permissions {
if !isValidPermission(perm) {
errors = append(errors, ValidationError{
Code: "INVALID_PERMISSION",
Message: fmt.Sprintf("Unknown permission: %s", perm),
})
}
}
// Validate orientation if specified
if m.Orientation != "" {
validOrientations := map[string]bool{"portrait": true, "landscape": true, "any": true}
if !validOrientations[m.Orientation] {
errors = append(errors, ValidationError{
Code: "INVALID_ORIENTATION",
Message: "Orientation must be one of: portrait, landscape, any",
})
}
}
// Validate background color if specified
if m.BackgroundColor != "" && !isValidHexColor(m.BackgroundColor) {
errors = append(errors, ValidationError{
Code: "INVALID_BACKGROUND_COLOR",
Message: "background_color must be a valid hex color (e.g., #FFFFFF)",
})
}
return errors
}
// Package ID pattern: reverse domain notation
var packageIDPattern = regexp.MustCompile(`^[a-z][a-z0-9]*(\.[a-z][a-z0-9_]*)+$`)
func isValidPackageID(id string) bool {
return packageIDPattern.MatchString(id)
}
// Semver pattern: X.Y.Z
var semverPattern = regexp.MustCompile(`^\d+\.\d+\.\d+$`)
func isValidSemver(version string) bool {
return semverPattern.MatchString(version)
}
// Hex color pattern: #RGB or #RRGGBB
var hexColorPattern = regexp.MustCompile(`^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$`)
func isValidHexColor(color string) bool {
return hexColorPattern.MatchString(color)
}
// Valid permissions
var validPermissions = map[string]bool{
"storage": true,
"network": true,
"camera": true,
"microphone": true,
"location": true,
"contacts": true,
"calendar": true,
"sensors": true,
}
func isValidPermission(perm string) bool {
return validPermissions[perm]
}