Implement controlled app launch (T-401)
This commit is contained in:
@@ -39,12 +39,16 @@ type InstalledFile struct {
|
||||
|
||||
// InstalledApp is the local installed-app.json v1 protocol.
|
||||
type InstalledApp struct {
|
||||
SchemaVersion int `json:"schema_version"`
|
||||
ID string `json:"id"`
|
||||
Version string `json:"version"`
|
||||
Architecture string `json:"architecture"`
|
||||
Channel string `json:"channel"`
|
||||
Files []InstalledFile `json:"files"`
|
||||
SchemaVersion int `json:"schema_version"`
|
||||
ID string `json:"id"`
|
||||
Version string `json:"version"`
|
||||
Architecture string `json:"architecture"`
|
||||
Channel string `json:"channel"`
|
||||
Entrypoint string `json:"entrypoint,omitempty"`
|
||||
WorkingDirectory string `json:"working_directory,omitempty"`
|
||||
MinOS string `json:"min_os,omitempty"`
|
||||
RequiresAdmin bool `json:"requires_admin"`
|
||||
Files []InstalledFile `json:"files"`
|
||||
}
|
||||
|
||||
// InstallationSnapshot contains disk facts without deriving UI status.
|
||||
@@ -107,6 +111,37 @@ func (store *InstalledAppStore) Read(appID string) (record InstalledApp, found b
|
||||
return store.readLocked(appID)
|
||||
}
|
||||
|
||||
// ResolveCurrent returns a validated record and the real current directory
|
||||
// below its app root. It never creates directories and rejects unsafe layouts.
|
||||
func (store *InstalledAppStore) ResolveCurrent(appID string) (InstalledApp, string, error) {
|
||||
store.mu.Lock()
|
||||
defer store.mu.Unlock()
|
||||
|
||||
record, found, err := store.readLocked(appID)
|
||||
if err != nil {
|
||||
return InstalledApp{}, "", err
|
||||
}
|
||||
if !found {
|
||||
return InstalledApp{}, "", os.ErrNotExist
|
||||
}
|
||||
appRoot, exists, err := store.inspectAppRoot(appID)
|
||||
if err != nil {
|
||||
return InstalledApp{}, "", err
|
||||
}
|
||||
if !exists {
|
||||
return InstalledApp{}, "", os.ErrNotExist
|
||||
}
|
||||
current := filepath.Join(appRoot, "current")
|
||||
if err := requireRealDirectory(current); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return InstalledApp{}, "", fmt.Errorf("%w: current directory is missing: %w", ErrStorageLayoutUnsafe, err)
|
||||
}
|
||||
return InstalledApp{}, "", err
|
||||
}
|
||||
record.Files = append([]InstalledFile(nil), record.Files...)
|
||||
return record, current, nil
|
||||
}
|
||||
|
||||
// Inspect reads the installed record and detects an unfinished install journal.
|
||||
func (store *InstalledAppStore) Inspect(appID string) (InstallationSnapshot, error) {
|
||||
store.mu.Lock()
|
||||
@@ -283,6 +318,20 @@ func (record InstalledApp) validate() error {
|
||||
if record.Channel != "stable" {
|
||||
return fmt.Errorf("%w: channel=%q", ErrInstalledAppInvalid, record.Channel)
|
||||
}
|
||||
if record.Entrypoint != "" {
|
||||
if err := safepath.ValidateRelative(record.Entrypoint); err != nil {
|
||||
return fmt.Errorf("%w: entrypoint: %v", ErrInstalledAppInvalid, err)
|
||||
}
|
||||
}
|
||||
if record.WorkingDirectory != "" && record.WorkingDirectory != "." {
|
||||
if err := safepath.ValidateRelative(record.WorkingDirectory); err != nil {
|
||||
return fmt.Errorf("%w: working_directory: %v", ErrInstalledAppInvalid, err)
|
||||
}
|
||||
}
|
||||
if record.MinOS != "" && record.MinOS != "windows-7-sp1" &&
|
||||
record.MinOS != "windows-10" && record.MinOS != "windows-11" {
|
||||
return fmt.Errorf("%w: min_os=%q", ErrInstalledAppInvalid, record.MinOS)
|
||||
}
|
||||
if record.Files == nil {
|
||||
return fmt.Errorf("%w: files must be an array", ErrInstalledAppInvalid)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user