package updater import ( "fmt" "os" "path/filepath" "strings" ) const ( appDirectoryName = "app" stagingDirectoryName = "staging" backupsDirectoryName = "backups" transactionFileName = "self-update-transaction.json" healthFileName = "self-update-health.json" transactionSchemaVersion = 1 ) type updateLayout struct { root string target string staging string backup string transaction string health string requestID string } func inspectRequest(request Request) (updateLayout, error) { if request.ParentPID <= 0 { return updateLayout{}, fmt.Errorf("%w: parent PID must be positive", ErrInvalidRequest) } if !validRequestID(request.RequestID) { return updateLayout{}, fmt.Errorf("%w: unsafe request ID", ErrInvalidRequest) } layout, err := inspectTarget(request.TargetDir) if err != nil { return updateLayout{}, err } if err := requireRealDirectory(layout.target, "target"); err != nil { return updateLayout{}, err } layout.requestID = request.RequestID expectedStaging := filepath.Join(layout.root, stagingDirectoryName, request.RequestID) if !filepath.IsAbs(request.StagingDir) || !samePath(request.StagingDir, expectedStaging) { return updateLayout{}, fmt.Errorf("%w: staging path does not match fixed layout", ErrUnsafeLayout) } layout.staging = expectedStaging layout.backup = filepath.Join(layout.root, backupsDirectoryName, request.RequestID) if err := requireRealDirectory(filepath.Join(layout.root, stagingDirectoryName), "staging parent"); err != nil { return updateLayout{}, err } if err := requireRealDirectory(layout.staging, "staging"); err != nil { if os.IsNotExist(unwrapPathError(err)) { return updateLayout{}, ErrStagingMissing } return updateLayout{}, err } return layout, nil } func inspectTarget(targetDir string) (updateLayout, error) { if !filepath.IsAbs(targetDir) { return updateLayout{}, fmt.Errorf("%w: target path must be absolute", ErrUnsafeLayout) } target := filepath.Clean(targetDir) if filepath.Base(target) != appDirectoryName { return updateLayout{}, fmt.Errorf("%w: target must be root/app", ErrUnsafeLayout) } root := filepath.Dir(target) if filepath.Base(root) == "." || root == target { return updateLayout{}, fmt.Errorf("%w: target root is invalid", ErrUnsafeLayout) } if err := requireRealDirectory(root, "root"); err != nil { return updateLayout{}, err } if exists, err := realDirectoryState(target, "target"); err != nil { return updateLayout{}, err } else if !exists { // target can be absent while recovery restores a backed-up app. return updateLayout{root: root, target: target, transaction: filepath.Join(root, transactionFileName), health: filepath.Join(root, healthFileName)}, nil } return updateLayout{root: root, target: target, transaction: filepath.Join(root, transactionFileName), health: filepath.Join(root, healthFileName)}, nil } func (layout updateLayout) validateReady(syncer DirectorySyncer) error { if err := requireRealDirectory(layout.target, "target"); err != nil { return err } if err := requireRealDirectory(layout.staging, "staging"); err != nil { return err } if err := verifyTreeNoLinks(layout.staging); err != nil { return err } if err := validateRegularFile(filepath.Join(layout.staging, ProductExecutableName)); err != nil { return fmt.Errorf("%w: staged %s: %v", ErrUnsafeLayout, ProductExecutableName, err) } backups := filepath.Dir(layout.backup) if info, err := os.Lstat(backups); os.IsNotExist(err) { if err := os.Mkdir(backups, 0o700); err != nil { return fmt.Errorf("create backups directory: %w", err) } if err := syncer.SyncDirectory(layout.root); err != nil { return fmt.Errorf("sync root after creating backups directory: %w", err) } } else if err != nil { return fmt.Errorf("%w: inspect backups parent: %v", ErrUnsafeLayout, err) } else if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() { return fmt.Errorf("%w: backups parent is not a real directory", ErrUnsafeLayout) } if exists, err := realDirectoryState(layout.backup, "backup"); err != nil { return err } else if exists { return ErrBackupExists } return nil } func (layout updateLayout) entrypoint() string { return filepath.Join(layout.target, ProductExecutableName) } func requireRealDirectory(path string, description string) error { exists, err := realDirectoryState(path, description) if err != nil { return err } if !exists { return &os.PathError{Op: "lstat", Path: path, Err: os.ErrNotExist} } return nil } func realDirectoryState(path string, description string) (bool, error) { info, err := os.Lstat(path) if os.IsNotExist(err) { return false, nil } if err != nil { return false, fmt.Errorf("%w: inspect %s: %v", ErrUnsafeLayout, description, err) } if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() { return false, fmt.Errorf("%w: %s is not a real directory", ErrUnsafeLayout, description) } return true, nil } func validateRegularFile(path string) error { info, err := os.Lstat(path) if err != nil { return err } if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() { return fmt.Errorf("file is not a regular non-symlink file") } return nil } func validRequestID(value string) bool { if len(value) < 8 || len(value) > 64 || strings.HasPrefix(value, "-") || strings.HasSuffix(value, "-") { return false } for _, char := range value { if (char < 'a' || char > 'z') && (char < '0' || char > '9') && char != '-' { return false } } return true } func samePath(left string, right string) bool { return filepath.Clean(left) == filepath.Clean(right) } func unwrapPathError(err error) error { for { pathErr, ok := err.(*os.PathError) if !ok { return err } err = pathErr.Err } }