186 lines
5.2 KiB
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]
|
|
}
|