• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

UiPath / uipathcli / 22829811383

08 Mar 2026 09:02PM UTC coverage: 90.055% (-0.9%) from 90.914%
22829811383

Pull #212

github

Chibi Vikram
Fix pack file size reporting by flushing zip before stat

Close zip writer and file before os.Stat to ensure all data
is flushed to disk, fixing TestPackReportsFileSize on all platforms.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pull Request #212: Add studio solution commands, skill, and agents.md

543 of 674 new or added lines in 16 files covered. (80.56%)

3 existing lines in 1 file now uncovered.

7317 of 8125 relevant lines covered (90.06%)

1.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.8
/plugin/studio/solution/unpack/solution_unpack_command.go
1
// Package unpack implements the command plugin for extracting a .uis file
2
// (ZIP archive) into a solution directory.
3
package unpack
4

5
import (
6
        "archive/zip"
7
        "bytes"
8
        "encoding/json"
9
        "errors"
10
        "fmt"
11
        "io"
12
        "net/http"
13
        "os"
14
        "path/filepath"
15
        "strings"
16

17
        "github.com/UiPath/uipathcli/log"
18
        "github.com/UiPath/uipathcli/output"
19
        "github.com/UiPath/uipathcli/plugin"
20
)
21

22
// The SolutionUnpackCommand extracts a .uis file into a directory.
23
type SolutionUnpackCommand struct{}
24

25
func (c SolutionUnpackCommand) Command() plugin.Command {
1✔
26
        return *plugin.NewCommand("studio").
1✔
27
                WithCategory("solution", "UiPath Solution management", "Pack, unpack, push and pull UiPath Maestro solutions.").
1✔
28
                WithOperation("unpack", "Unpack Solution", "Extracts a .uis file into a solution directory").
1✔
29
                WithParameter(plugin.NewParameter("source", plugin.ParameterTypeString, "Path to .uis file").
1✔
30
                        WithRequired(true)).
1✔
31
                WithParameter(plugin.NewParameter("destination", plugin.ParameterTypeString, "Output directory path").
1✔
32
                        WithDefaultValue(""))
1✔
33
}
1✔
34

35
func (c SolutionUnpackCommand) Execute(ctx plugin.ExecutionContext, writer output.OutputWriter, logger log.Logger) error {
1✔
36
        source := c.getStringParameter("source", "", ctx.Parameters)
1✔
37
        if source == "" {
1✔
NEW
38
                return errors.New("Source .uis file is required")
×
NEW
39
        }
×
40
        source, _ = filepath.Abs(source)
1✔
41
        destination := c.getStringParameter("destination", "", ctx.Parameters)
1✔
42

1✔
43
        if _, err := os.Stat(source); err != nil {
2✔
44
                return fmt.Errorf("File not found: %s", source)
1✔
45
        }
1✔
46

47
        if destination == "" {
2✔
48
                basename := filepath.Base(source)
1✔
49
                destination = strings.TrimSuffix(basename, filepath.Ext(basename))
1✔
50
        }
1✔
51
        destination, _ = filepath.Abs(destination)
1✔
52

1✔
53
        params := newSolutionUnpackParams(source, destination)
1✔
54
        result, err := c.unpack(*params)
1✔
55
        if err != nil {
2✔
56
                return err
1✔
57
        }
1✔
58

59
        jsonData, err := json.Marshal(result)
1✔
60
        if err != nil {
1✔
NEW
61
                return fmt.Errorf("Unpack command failed: %w", err)
×
NEW
62
        }
×
63
        return writer.WriteResponse(*output.NewResponseInfo(http.StatusOK, "200 OK", "HTTP/1.1", map[string][]string{}, bytes.NewReader(jsonData)))
1✔
64
}
65

66
func (c SolutionUnpackCommand) unpack(params solutionUnpackParams) (*solutionUnpackResult, error) {
1✔
67
        reader, err := zip.OpenReader(params.Source)
1✔
68
        if err != nil {
2✔
69
                return nil, fmt.Errorf("Cannot open .uis file: %w", err)
1✔
70
        }
1✔
71
        defer func() { _ = reader.Close() }()
2✔
72

73
        for _, file := range reader.File {
2✔
74
                err := c.extractFile(file, params.Destination)
1✔
75
                if err != nil {
2✔
76
                        return nil, err
1✔
77
                }
1✔
NEW
78
        }
×
NEW
79

×
80
        solutionId, projectCount := c.readSolutionInfo(filepath.Join(params.Destination, "SolutionStorage.json"))
1✔
81

1✔
82
        return newSucceededSolutionUnpackResult(params.Destination, solutionId, projectCount), nil
1✔
NEW
83
}
×
NEW
84

×
NEW
85
const maxArchiveFileSize = 1 * 1024 * 1024 * 1024
×
NEW
86

×
87
func (c SolutionUnpackCommand) extractFile(file *zip.File, destination string) error {
1✔
88
        destPath, err := c.sanitizeArchivePath(destination, file.Name)
1✔
89
        if err != nil {
2✔
90
                return err
1✔
91
        }
1✔
NEW
92

×
93
        if file.FileInfo().IsDir() {
1✔
NEW
94
                return os.MkdirAll(destPath, 0750)
×
NEW
95
        }
×
NEW
96

×
97
        err = os.MkdirAll(filepath.Dir(destPath), 0750)
1✔
98
        if err != nil {
1✔
NEW
99
                return fmt.Errorf("Cannot create directory for '%s': %w", destPath, err)
×
NEW
100
        }
×
NEW
101

×
102
        rc, err := file.Open()
1✔
103
        if err != nil {
1✔
NEW
104
                return fmt.Errorf("Cannot read archive entry '%s': %w", file.Name, err)
×
NEW
105
        }
×
106
        defer func() { _ = rc.Close() }()
2✔
NEW
107

×
108
        outFile, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
1✔
109
        if err != nil {
1✔
NEW
110
                return fmt.Errorf("Cannot create file '%s': %w", destPath, err)
×
NEW
111
        }
×
112
        defer func() { _ = outFile.Close() }()
2✔
113

114
        _, err = io.CopyN(outFile, rc, maxArchiveFileSize)
1✔
115
        if err != nil && !errors.Is(err, io.EOF) {
1✔
NEW
116
                return fmt.Errorf("Error extracting '%s': %w", file.Name, err)
×
NEW
117
        }
×
118
        return nil
1✔
119
}
NEW
120

×
121
func (c SolutionUnpackCommand) sanitizeArchivePath(directory string, name string) (string, error) {
1✔
122
        result := filepath.Join(directory, name)
1✔
123
        if strings.HasPrefix(result, filepath.Clean(directory)) {
2✔
124
                return result, nil
1✔
125
        }
1✔
126
        return "", fmt.Errorf("File path '%s' is not allowed", name)
1✔
NEW
127
}
×
NEW
128

×
129
func (c SolutionUnpackCommand) readSolutionInfo(path string) (string, int) {
1✔
130
        data, err := os.ReadFile(path)
1✔
131
        if err != nil {
1✔
NEW
132
                return "", 0
×
NEW
133
        }
×
134
        var storage struct {
1✔
135
                SolutionId string `json:"SolutionId"`
1✔
136
                Projects   []struct {
1✔
137
                        ProjectId string `json:"ProjectId"`
1✔
138
                } `json:"Projects"`
1✔
139
        }
1✔
140
        err = json.Unmarshal(data, &storage)
1✔
141
        if err != nil {
1✔
NEW
142
                return "", 0
×
NEW
143
        }
×
144
        return storage.SolutionId, len(storage.Projects)
1✔
145
}
146

147
func (c SolutionUnpackCommand) getStringParameter(name string, defaultValue string, parameters []plugin.ExecutionParameter) string {
1✔
148
        result := defaultValue
1✔
149
        for _, p := range parameters {
2✔
150
                if p.Name == name {
2✔
151
                        if data, ok := p.Value.(string); ok {
2✔
152
                                result = data
1✔
153
                                break
1✔
154
                        }
155
                }
156
        }
157
        return result
1✔
158
}
159

160
func NewSolutionUnpackCommand() *SolutionUnpackCommand {
1✔
161
        return &SolutionUnpackCommand{}
1✔
162
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc