// 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] }