add mosis-portal Go project with package signing and validation
This commit is contained in:
185
portal/pkg/mospkg/manifest.go
Normal file
185
portal/pkg/mospkg/manifest.go
Normal file
@@ -0,0 +1,185 @@
|
||||
// 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]
|
||||
}
|
||||
Reference in New Issue
Block a user