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

309 lines
7.7 KiB
Go

// Package mospkg provides functionality for Mosis app packages (.mosis files)
package mospkg
import (
"archive/zip"
"fmt"
"io"
"path/filepath"
"strings"
)
// Size limits
const (
MaxPackageSize = 50 * 1024 * 1024 // 50 MB
MaxFileSize = 10 * 1024 * 1024 // 10 MB
MaxFileCount = 1000
MaxPathLength = 256
MaxManifestSize = 64 * 1024 // 64 KB
)
// ValidationError represents a validation error
type ValidationError struct {
Code string `json:"code"`
Message string `json:"message"`
File string `json:"file,omitempty"`
Line int `json:"line,omitempty"`
}
// ValidationWarning represents a non-blocking validation warning
type ValidationWarning struct {
Code string `json:"code"`
Message string `json:"message"`
File string `json:"file,omitempty"`
}
// ValidationResult holds the result of package validation
type ValidationResult struct {
Valid bool `json:"valid"`
Errors []ValidationError `json:"errors,omitempty"`
Warnings []ValidationWarning `json:"warnings,omitempty"`
Manifest *Manifest `json:"manifest,omitempty"`
}
// AllowedExtensions are file extensions permitted in packages
var AllowedExtensions = map[string]bool{
".rml": true,
".rcss": true,
".lua": true,
".png": true,
".jpg": true,
".jpeg": true,
".tga": true,
".webp": true,
".ttf": true,
".otf": true,
".json": true,
".ogg": true,
".wav": true,
".mp3": true,
}
// ForbiddenExtensions are file extensions not allowed in packages
var ForbiddenExtensions = map[string]bool{
".exe": true,
".dll": true,
".so": true,
".dylib": true,
".sh": true,
".bat": true,
".ps1": true,
".py": true,
".js": true,
".zip": true,
".tar": true,
".gz": true,
}
// ValidatePackage validates a .mosis package file
func ValidatePackage(path string) (*ValidationResult, error) {
result := &ValidationResult{Valid: true}
// Open ZIP archive
reader, err := zip.OpenReader(path)
if err != nil {
result.Valid = false
result.Errors = append(result.Errors, ValidationError{
Code: "INVALID_ZIP",
Message: fmt.Sprintf("Failed to open package: %v", err),
})
return result, nil
}
defer reader.Close()
// Check file count
if len(reader.File) > MaxFileCount {
result.Valid = false
result.Errors = append(result.Errors, ValidationError{
Code: "TOO_MANY_FILES",
Message: fmt.Sprintf("Package contains %d files, maximum is %d", len(reader.File), MaxFileCount),
})
return result, nil
}
var manifestData []byte
seenFiles := make(map[string]bool)
var totalSize int64
for _, file := range reader.File {
name := file.Name
// Check for path traversal
if strings.Contains(name, "..") {
result.Valid = false
result.Errors = append(result.Errors, ValidationError{
Code: "PATH_TRAVERSAL",
Message: "Path traversal detected",
File: name,
})
continue
}
// Check for absolute paths
if filepath.IsAbs(name) {
result.Valid = false
result.Errors = append(result.Errors, ValidationError{
Code: "ABSOLUTE_PATH",
Message: "Absolute path not allowed",
File: name,
})
continue
}
// Check path length
if len(name) > MaxPathLength {
result.Valid = false
result.Errors = append(result.Errors, ValidationError{
Code: "PATH_TOO_LONG",
Message: fmt.Sprintf("Path exceeds %d characters", MaxPathLength),
File: name,
})
continue
}
// Check for duplicates
normalizedName := strings.ToLower(name)
if seenFiles[normalizedName] {
result.Valid = false
result.Errors = append(result.Errors, ValidationError{
Code: "DUPLICATE_FILE",
Message: "Duplicate file detected",
File: name,
})
continue
}
seenFiles[normalizedName] = true
// Skip directories
if file.FileInfo().IsDir() {
continue
}
// Check individual file size
if file.UncompressedSize64 > uint64(MaxFileSize) {
result.Valid = false
result.Errors = append(result.Errors, ValidationError{
Code: "FILE_TOO_LARGE",
Message: fmt.Sprintf("File exceeds %d MB limit", MaxFileSize/(1024*1024)),
File: name,
})
continue
}
totalSize += int64(file.UncompressedSize64)
// Check file extension
ext := strings.ToLower(filepath.Ext(name))
if ForbiddenExtensions[ext] {
result.Valid = false
result.Errors = append(result.Errors, ValidationError{
Code: "FORBIDDEN_EXTENSION",
Message: fmt.Sprintf("File type %s is not allowed", ext),
File: name,
})
continue
}
// Skip META-INF files for extension check
if !strings.HasPrefix(name, "META-INF/") && ext != "" && !AllowedExtensions[ext] {
result.Warnings = append(result.Warnings, ValidationWarning{
Code: "UNKNOWN_EXTENSION",
Message: fmt.Sprintf("Unknown file extension: %s", ext),
File: name,
})
}
// Read manifest.json
if name == "manifest.json" {
rc, err := file.Open()
if err != nil {
result.Valid = false
result.Errors = append(result.Errors, ValidationError{
Code: "MANIFEST_READ_ERROR",
Message: fmt.Sprintf("Failed to read manifest: %v", err),
})
continue
}
manifestData, err = io.ReadAll(io.LimitReader(rc, MaxManifestSize))
rc.Close()
if err != nil {
result.Valid = false
result.Errors = append(result.Errors, ValidationError{
Code: "MANIFEST_READ_ERROR",
Message: fmt.Sprintf("Failed to read manifest: %v", err),
})
}
}
}
// Check total size
if totalSize > MaxPackageSize {
result.Valid = false
result.Errors = append(result.Errors, ValidationError{
Code: "PACKAGE_TOO_LARGE",
Message: fmt.Sprintf("Package exceeds %d MB limit", MaxPackageSize/(1024*1024)),
})
}
// Validate manifest
if manifestData == nil {
result.Valid = false
result.Errors = append(result.Errors, ValidationError{
Code: "MISSING_MANIFEST",
Message: "Package is missing manifest.json",
})
} else {
manifest, err := ParseManifest(manifestData)
if err != nil {
result.Valid = false
result.Errors = append(result.Errors, ValidationError{
Code: "INVALID_MANIFEST",
Message: fmt.Sprintf("Invalid manifest.json: %v", err),
})
} else {
result.Manifest = manifest
manifestErrors := manifest.Validate()
if len(manifestErrors) > 0 {
result.Valid = false
result.Errors = append(result.Errors, manifestErrors...)
}
// Check that entry point exists
entryExists := false
for _, file := range reader.File {
if file.Name == manifest.Entry {
entryExists = true
break
}
}
if !entryExists {
result.Valid = false
result.Errors = append(result.Errors, ValidationError{
Code: "MISSING_ENTRY",
Message: fmt.Sprintf("Entry point file not found: %s", manifest.Entry),
})
}
// Check icons
if result.Manifest.Icons.Size32 != "" {
if !fileExistsInZip(reader, result.Manifest.Icons.Size32) {
result.Errors = append(result.Errors, ValidationError{
Code: "MISSING_ICON",
Message: "Icon file not found",
File: result.Manifest.Icons.Size32,
})
}
}
if result.Manifest.Icons.Size64 != "" {
if !fileExistsInZip(reader, result.Manifest.Icons.Size64) {
result.Errors = append(result.Errors, ValidationError{
Code: "MISSING_ICON",
Message: "Icon file not found",
File: result.Manifest.Icons.Size64,
})
}
}
if result.Manifest.Icons.Size128 != "" {
if !fileExistsInZip(reader, result.Manifest.Icons.Size128) {
result.Errors = append(result.Errors, ValidationError{
Code: "MISSING_ICON",
Message: "Icon file not found",
File: result.Manifest.Icons.Size128,
})
}
}
}
}
return result, nil
}
func fileExistsInZip(reader *zip.ReadCloser, name string) bool {
for _, file := range reader.File {
if file.Name == name {
return true
}
}
return false
}