ttsc 0.32.1 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cmd/ttsc-cache/account.go +94 -0
- package/cmd/ttsc-cache/interruption_test.go +176 -0
- package/cmd/ttsc-cache/main.go +252 -0
- package/cmd/ttsc-cache/merge.go +324 -0
- package/cmd/ttsc-cache/merge_test.go +161 -0
- package/cmd/ttsc-cache/prepare.go +330 -0
- package/cmd/ttsc-cache/prepare_test.go +456 -0
- package/cmd/ttsc-cache/protocol_test.go +119 -0
- package/cmd/ttsc-cache/publisher_seal_test.go +135 -0
- package/cmd/ttsc-cache/seal.go +427 -0
- package/cmd/ttsc-cache/stock_go_failure_test.go +94 -0
- package/cmd/ttsc-cache/stock_go_test.go +163 -0
- package/cmd/ttsc-cache/store.go +249 -0
- package/cmd/ttsc-cache/store_test.go +104 -0
- package/cmd/ttsc-cache/tuple.go +107 -0
- package/cmd/ttsc-cache/tuple_test.go +62 -0
- package/cmd/ttsc-cache/usage.go +73 -0
- package/driver/configloaderscratch/canonical_directory_other.go +34 -0
- package/driver/configloaderscratch/canonical_directory_windows.go +60 -0
- package/driver/configloaderscratch/identity_temp_base.go +81 -0
- package/driver/configloaderscratch/identity_temp_base_posix_test.go +124 -0
- package/driver/configloaderscratch/identity_temp_base_windows_test.go +156 -0
- package/driver/emit_provenance.go +7 -5
- package/driver/resolutioninputs/recorder.cjs +16 -4
- package/driver/rewrite.go +25 -16
- package/driver/write_file_if_changed.go +69 -0
- package/driver/write_file_if_changed_test.go +148 -0
- package/internal/packageartifacts/manifest.go +288 -0
- package/internal/packageartifacts/manifest_test.go +127 -0
- package/lib/compiler/internal/build/BuildExecution.d.ts +11 -7
- package/lib/compiler/internal/build/BuildExecution.js +22 -8
- package/lib/compiler/internal/build/BuildExecution.js.map +1 -1
- package/lib/compiler/internal/build/ResidentCheckWatchSession.d.ts +10 -10
- package/lib/compiler/internal/build/ResidentCheckWatchSession.js +19 -14
- package/lib/compiler/internal/build/ResidentCheckWatchSession.js.map +1 -1
- package/lib/compiler/internal/build/RunBuildOptions.d.ts +8 -1
- package/lib/compiler/internal/build/TsgoArguments.d.ts +16 -0
- package/lib/compiler/internal/build/TsgoArguments.js +19 -0
- package/lib/compiler/internal/build/TsgoArguments.js.map +1 -1
- package/lib/compiler/internal/captureProcessOutput.d.ts +16 -13
- package/lib/compiler/internal/captureProcessOutput.js +22 -14
- package/lib/compiler/internal/captureProcessOutput.js.map +1 -1
- package/lib/compiler/internal/startResidentTransform.js +3 -1
- package/lib/compiler/internal/startResidentTransform.js.map +1 -1
- package/lib/compiler/internal/transformProjectInMemory.js +5 -1
- package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
- package/lib/launcher/internal/runtime/PluginDescriptorInputReport.d.ts +43 -0
- package/lib/launcher/internal/runtime/PluginDescriptorInputReport.js +106 -0
- package/lib/launcher/internal/runtime/PluginDescriptorInputReport.js.map +1 -0
- package/lib/launcher/internal/runtime/installRuntimeHooks.js +15 -29
- package/lib/launcher/internal/runtime/installRuntimeHooks.js.map +1 -1
- package/lib/launcher/internal/ttscserver/runTtscserver.js +1 -0
- package/lib/launcher/internal/ttscserver/runTtscserver.js.map +1 -1
- package/lib/launcher/internal/watch/WatchTopology.js +5 -4
- package/lib/launcher/internal/watch/WatchTopology.js.map +1 -1
- package/lib/plugin/internal/CapabilityResolutionFormat.js +4 -3
- package/lib/plugin/internal/CapabilityResolutionFormat.js.map +1 -1
- package/lib/plugin/internal/load/COMMONJS_PLUGIN_DESCRIPTOR_SHIM_SOURCE.js +6 -0
- package/lib/plugin/internal/load/COMMONJS_PLUGIN_DESCRIPTOR_SHIM_SOURCE.js.map +1 -1
- package/lib/plugin/internal/load/PLUGIN_DESCRIPTOR_SHIM_SOURCE.js +6 -0
- package/lib/plugin/internal/load/PLUGIN_DESCRIPTOR_SHIM_SOURCE.js.map +1 -1
- package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.js +3 -3
- package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.js.map +1 -1
- package/lib/plugin/internal/load/descriptorRecordEnvironment.d.ts +15 -0
- package/lib/plugin/internal/load/descriptorRecordEnvironment.js +24 -0
- package/lib/plugin/internal/load/descriptorRecordEnvironment.js.map +1 -0
- package/lib/plugin/internal/load/loadProjectPlugins.js +28 -26
- package/lib/plugin/internal/load/loadProjectPlugins.js.map +1 -1
- package/lib/plugin/internal/source/SourceBuildCacheLayout.d.ts +2 -0
- package/lib/plugin/internal/source/SourceBuildCacheLayout.js +2 -0
- package/lib/plugin/internal/source/SourceBuildCacheLayout.js.map +1 -1
- package/lib/plugin/internal/source/SourcePackageArtifacts.d.ts +145 -0
- package/lib/plugin/internal/source/SourcePackageArtifacts.js +612 -0
- package/lib/plugin/internal/source/SourcePackageArtifacts.js.map +1 -0
- package/lib/plugin/internal/source/buildSourcePlugin.js +54 -4
- package/lib/plugin/internal/source/buildSourcePlugin.js.map +1 -1
- package/lib/plugin/internal/source/prunePackageArtifactRoot.d.ts +19 -0
- package/lib/plugin/internal/source/prunePackageArtifactRoot.js +95 -0
- package/lib/plugin/internal/source/prunePackageArtifactRoot.js.map +1 -0
- package/lib/plugin/internal/source/resolveCleanTargets.js +1 -1
- package/lib/plugin/internal/source/resolveCleanTargets.js.map +1 -1
- package/lib/plugin/internal/source/resolveSourceBuildCachePaths.js +9 -4
- package/lib/plugin/internal/source/resolveSourceBuildCachePaths.js.map +1 -1
- package/lib/plugin/internal/source/selectedRecordIdentityTempParent.d.ts +18 -0
- package/lib/plugin/internal/source/selectedRecordIdentityTempParent.js +46 -0
- package/lib/plugin/internal/source/selectedRecordIdentityTempParent.js.map +1 -0
- package/package.json +8 -8
- package/src/compiler/internal/build/BuildExecution.ts +30 -10
- package/src/compiler/internal/build/ResidentCheckWatchSession.ts +23 -22
- package/src/compiler/internal/build/RunBuildOptions.ts +9 -1
- package/src/compiler/internal/build/TsgoArguments.ts +22 -0
- package/src/compiler/internal/captureProcessOutput.ts +28 -14
- package/src/compiler/internal/startResidentTransform.ts +7 -1
- package/src/compiler/internal/transformProjectInMemory.ts +9 -1
- package/src/launcher/internal/runtime/PluginDescriptorInputReport.ts +100 -0
- package/src/launcher/internal/runtime/installRuntimeHooks.ts +57 -55
- package/src/launcher/internal/ttscserver/runTtscserver.ts +5 -0
- package/src/launcher/internal/watch/WatchTopology.ts +5 -4
- package/src/plugin/internal/CapabilityResolutionFormat.ts +4 -3
- package/src/plugin/internal/load/COMMONJS_PLUGIN_DESCRIPTOR_SHIM_SOURCE.ts +4 -0
- package/src/plugin/internal/load/PLUGIN_DESCRIPTOR_SHIM_SOURCE.ts +4 -0
- package/src/plugin/internal/load/PluginDescriptorEvaluationCache.ts +3 -3
- package/src/plugin/internal/load/descriptorRecordEnvironment.ts +24 -0
- package/src/plugin/internal/load/loadProjectPlugins.ts +45 -29
- package/src/plugin/internal/source/SourceBuildCacheLayout.ts +3 -0
- package/src/plugin/internal/source/SourcePackageArtifacts.ts +787 -0
- package/src/plugin/internal/source/buildSourcePlugin.ts +82 -12
- package/src/plugin/internal/source/prunePackageArtifactRoot.ts +100 -0
- package/src/plugin/internal/source/resolveCleanTargets.ts +1 -0
- package/src/plugin/internal/source/resolveSourceBuildCachePaths.ts +11 -4
- package/src/plugin/internal/source/selectedRecordIdentityTempParent.ts +40 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"archive/zip"
|
|
5
|
+
"context"
|
|
6
|
+
"crypto/sha256"
|
|
7
|
+
"encoding/hex"
|
|
8
|
+
"errors"
|
|
9
|
+
"fmt"
|
|
10
|
+
"io"
|
|
11
|
+
"net"
|
|
12
|
+
"net/http"
|
|
13
|
+
"net/url"
|
|
14
|
+
"os"
|
|
15
|
+
"path/filepath"
|
|
16
|
+
"strings"
|
|
17
|
+
"time"
|
|
18
|
+
|
|
19
|
+
"github.com/samchon/ttsc/packages/ttsc/internal/packageartifacts"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
type preparationReceipt struct {
|
|
23
|
+
SelectedPacks []string `json:"selectedPacks"`
|
|
24
|
+
DownloadedPacks int `json:"downloadedPacks"`
|
|
25
|
+
Requests int `json:"requests"`
|
|
26
|
+
CompressedBytes int64 `json:"compressedBytes"`
|
|
27
|
+
DecodedBytes int64 `json:"decodedBytes"`
|
|
28
|
+
Storage artifactAccounting `json:"storage"`
|
|
29
|
+
DownloadMillis int64 `json:"downloadMillis"`
|
|
30
|
+
ExtractMillis int64 `json:"extractMillis"`
|
|
31
|
+
AdmissionMillis int64 `json:"admissionMillis"`
|
|
32
|
+
TotalMillis int64 `json:"totalMillis"`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const maxSelectedCompressedBytes int64 = 512 << 20
|
|
36
|
+
|
|
37
|
+
const maxSelectedDecodedBytes int64 = 4 << 30
|
|
38
|
+
|
|
39
|
+
const maxRetainedBaseBytes int64 = 8 << 30
|
|
40
|
+
|
|
41
|
+
const maxRetainedStagingBytes int64 = 5 << 30
|
|
42
|
+
|
|
43
|
+
func preparePacks(ctx context.Context, store *artifactStore, manifest packageartifacts.Manifest, names []string) (preparationReceipt, error) {
|
|
44
|
+
started := time.Now()
|
|
45
|
+
var receipt preparationReceipt
|
|
46
|
+
if len(names) == 0 || len(names) > 8 {
|
|
47
|
+
return receipt, errors.New("select between one and eight artifact packs")
|
|
48
|
+
}
|
|
49
|
+
packs := make(map[string]packageartifacts.Pack, len(manifest.Packs))
|
|
50
|
+
for _, pack := range manifest.Packs {
|
|
51
|
+
packs[pack.Name] = pack
|
|
52
|
+
}
|
|
53
|
+
seen := make(map[string]bool, len(names))
|
|
54
|
+
selected := make([]packageartifacts.Pack, 0, len(names))
|
|
55
|
+
var compressed, decoded int64
|
|
56
|
+
for _, name := range names {
|
|
57
|
+
pack, exists := packs[name]
|
|
58
|
+
if !exists || seen[name] {
|
|
59
|
+
return receipt, fmt.Errorf("unknown or repeated artifact pack %q", name)
|
|
60
|
+
}
|
|
61
|
+
seen[name] = true
|
|
62
|
+
receipt.SelectedPacks = append(receipt.SelectedPacks, name)
|
|
63
|
+
if pack.Length > maxSelectedCompressedBytes-compressed || pack.DecodedBytes > maxSelectedDecodedBytes-decoded {
|
|
64
|
+
return receipt, errors.New("selected artifact packs exceed transfer or decoded byte policy")
|
|
65
|
+
}
|
|
66
|
+
compressed += pack.Length
|
|
67
|
+
decoded += pack.DecodedBytes
|
|
68
|
+
selected = append(selected, pack)
|
|
69
|
+
}
|
|
70
|
+
for _, pack := range selected {
|
|
71
|
+
if admittedPack(store, pack, manifest.Actions) {
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
accounting, err := accountArtifactStore(store)
|
|
75
|
+
if err != nil {
|
|
76
|
+
return receipt, fmt.Errorf("accounting for artifact admission: %w", err)
|
|
77
|
+
}
|
|
78
|
+
if accounting.BaseBytes > maxRetainedBaseBytes-pack.DecodedBytes || accounting.StagingBytes > maxRetainedStagingBytes-pack.DecodedBytes-pack.Length {
|
|
79
|
+
return receipt, errors.New("artifact storage exceeds preparation byte policy")
|
|
80
|
+
}
|
|
81
|
+
downloaded, err := prepareSelectedPack(ctx, store, pack, manifest.Actions, &receipt)
|
|
82
|
+
if err != nil {
|
|
83
|
+
return receipt, err
|
|
84
|
+
}
|
|
85
|
+
if downloaded {
|
|
86
|
+
receipt.DownloadedPacks++
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
var err error
|
|
90
|
+
receipt.Storage, err = accountArtifactStore(store)
|
|
91
|
+
if err != nil {
|
|
92
|
+
return receipt, fmt.Errorf("accounting for prepared artifacts: %w", err)
|
|
93
|
+
}
|
|
94
|
+
receipt.TotalMillis = time.Since(started).Milliseconds()
|
|
95
|
+
return receipt, nil
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
func prepareSelectedPack(ctx context.Context, store *artifactStore, pack packageartifacts.Pack, actions []packageartifacts.Action, receipt *preparationReceipt) (bool, error) {
|
|
99
|
+
lock := filepath.ToSlash(filepath.Join("base", "staging", pack.Name+"-"+pack.SHA256+".lock"))
|
|
100
|
+
deadline := time.Now().Add(120 * time.Second)
|
|
101
|
+
for {
|
|
102
|
+
if err := ctx.Err(); err != nil {
|
|
103
|
+
return false, err
|
|
104
|
+
}
|
|
105
|
+
file, err := store.root.OpenFile(lock, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
|
|
106
|
+
if err == nil {
|
|
107
|
+
defer store.root.Remove(lock)
|
|
108
|
+
defer file.Close()
|
|
109
|
+
if admittedPack(store, pack, actions) {
|
|
110
|
+
return false, nil
|
|
111
|
+
}
|
|
112
|
+
if err := downloadAndAdmitPack(ctx, store, pack, actions, receipt); err != nil {
|
|
113
|
+
return false, err
|
|
114
|
+
}
|
|
115
|
+
return true, nil
|
|
116
|
+
}
|
|
117
|
+
if !errors.Is(err, os.ErrExist) {
|
|
118
|
+
return false, err
|
|
119
|
+
}
|
|
120
|
+
info, err := store.root.Lstat(lock)
|
|
121
|
+
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
122
|
+
return false, err
|
|
123
|
+
}
|
|
124
|
+
if err == nil && !info.Mode().IsRegular() {
|
|
125
|
+
return false, errors.New("artifact preparation lock is not an ordinary file")
|
|
126
|
+
}
|
|
127
|
+
if admittedPack(store, pack, actions) {
|
|
128
|
+
return false, nil
|
|
129
|
+
}
|
|
130
|
+
if time.Now().After(deadline) {
|
|
131
|
+
return false, fmt.Errorf("artifact preparation lock for %s did not settle", pack.Name)
|
|
132
|
+
}
|
|
133
|
+
select {
|
|
134
|
+
case <-ctx.Done():
|
|
135
|
+
return false, ctx.Err()
|
|
136
|
+
case <-time.After(50 * time.Millisecond):
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
func admittedPack(store *artifactStore, pack packageartifacts.Pack, actions []packageartifacts.Action) bool {
|
|
142
|
+
name := filepath.ToSlash(filepath.Join("base", "packs", pack.Name+"-"+pack.SHA256))
|
|
143
|
+
info, err := store.root.Lstat(name)
|
|
144
|
+
if err != nil || !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
|
|
145
|
+
return false
|
|
146
|
+
}
|
|
147
|
+
receipt, err := store.root.ReadFile(filepath.ToSlash(filepath.Join(name, ".complete")))
|
|
148
|
+
if err != nil || string(receipt) != pack.SHA256+"\n" {
|
|
149
|
+
return false
|
|
150
|
+
}
|
|
151
|
+
seen := make(map[string]bool)
|
|
152
|
+
for _, action := range actions {
|
|
153
|
+
if action.Pack != pack.Name || seen[action.OutputID] {
|
|
154
|
+
continue
|
|
155
|
+
}
|
|
156
|
+
seen[action.OutputID] = true
|
|
157
|
+
body, err := store.root.Lstat(filepath.ToSlash(filepath.Join(name, "bodies", action.OutputID)))
|
|
158
|
+
if err != nil || !body.Mode().IsRegular() || body.Size() != action.BodyLength {
|
|
159
|
+
return false
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return len(seen) == pack.FileCount
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
func downloadAndAdmitPack(ctx context.Context, store *artifactStore, pack packageartifacts.Pack, actions []packageartifacts.Action, receipt *preparationReceipt) error {
|
|
166
|
+
stage := filepath.ToSlash(filepath.Join("base", "staging", randomName()))
|
|
167
|
+
if err := store.root.Mkdir(stage, 0700); err != nil {
|
|
168
|
+
return err
|
|
169
|
+
}
|
|
170
|
+
defer store.root.RemoveAll(stage)
|
|
171
|
+
if err := store.root.Mkdir(filepath.ToSlash(filepath.Join(stage, "bodies")), 0700); err != nil {
|
|
172
|
+
return err
|
|
173
|
+
}
|
|
174
|
+
archive := filepath.ToSlash(filepath.Join(stage, "pack.zip"))
|
|
175
|
+
started := time.Now()
|
|
176
|
+
if err := downloadPack(ctx, store, pack, archive, receipt); err != nil {
|
|
177
|
+
return err
|
|
178
|
+
}
|
|
179
|
+
receipt.DownloadMillis += time.Since(started).Milliseconds()
|
|
180
|
+
started = time.Now()
|
|
181
|
+
if err := extractPack(store, pack, archive, stage, actions, receipt); err != nil {
|
|
182
|
+
return err
|
|
183
|
+
}
|
|
184
|
+
receipt.ExtractMillis += time.Since(started).Milliseconds()
|
|
185
|
+
started = time.Now()
|
|
186
|
+
if err := store.root.Remove(archive); err != nil {
|
|
187
|
+
return err
|
|
188
|
+
}
|
|
189
|
+
marker := filepath.ToSlash(filepath.Join(stage, ".complete"))
|
|
190
|
+
file, err := store.root.OpenFile(marker, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
|
|
191
|
+
if err != nil {
|
|
192
|
+
return err
|
|
193
|
+
}
|
|
194
|
+
_, writeErr := io.WriteString(file, pack.SHA256+"\n")
|
|
195
|
+
syncErr := file.Sync()
|
|
196
|
+
closeErr := file.Close()
|
|
197
|
+
if writeErr != nil || syncErr != nil || closeErr != nil {
|
|
198
|
+
return errors.Join(writeErr, syncErr, closeErr)
|
|
199
|
+
}
|
|
200
|
+
final := filepath.ToSlash(filepath.Join("base", "packs", pack.Name+"-"+pack.SHA256))
|
|
201
|
+
if err := store.root.Rename(stage, final); err != nil {
|
|
202
|
+
if admittedPack(store, pack, actions) {
|
|
203
|
+
return nil
|
|
204
|
+
}
|
|
205
|
+
return fmt.Errorf("admitting artifact pack %s: %w", pack.Name, err)
|
|
206
|
+
}
|
|
207
|
+
receipt.AdmissionMillis += time.Since(started).Milliseconds()
|
|
208
|
+
return nil
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
func downloadPack(ctx context.Context, store *artifactStore, pack packageartifacts.Pack, destination string, receipt *preparationReceipt) error {
|
|
212
|
+
client := &http.Client{
|
|
213
|
+
Timeout: 120 * time.Second,
|
|
214
|
+
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|
215
|
+
if len(via) >= 5 || !safeRedirect(req.URL) {
|
|
216
|
+
return errors.New("artifact redirect policy rejected destination")
|
|
217
|
+
}
|
|
218
|
+
receipt.Requests++
|
|
219
|
+
return nil
|
|
220
|
+
},
|
|
221
|
+
}
|
|
222
|
+
request, err := http.NewRequestWithContext(ctx, http.MethodGet, pack.URL, nil)
|
|
223
|
+
if err != nil {
|
|
224
|
+
return fmt.Errorf("invalid artifact pack URL for %s", pack.Name)
|
|
225
|
+
}
|
|
226
|
+
receipt.Requests++
|
|
227
|
+
response, err := client.Do(request)
|
|
228
|
+
if err != nil {
|
|
229
|
+
return fmt.Errorf("artifact pack %s transfer failed (%T)", pack.Name, err)
|
|
230
|
+
}
|
|
231
|
+
defer response.Body.Close()
|
|
232
|
+
if response.StatusCode != http.StatusOK {
|
|
233
|
+
return fmt.Errorf("artifact pack %s returned HTTP %d", pack.Name, response.StatusCode)
|
|
234
|
+
}
|
|
235
|
+
if response.ContentLength > pack.Length {
|
|
236
|
+
return fmt.Errorf("artifact pack %s exceeds compressed byte limit", pack.Name)
|
|
237
|
+
}
|
|
238
|
+
file, err := store.root.OpenFile(destination, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
|
|
239
|
+
if err != nil {
|
|
240
|
+
return err
|
|
241
|
+
}
|
|
242
|
+
hash := sha256.New()
|
|
243
|
+
count, copyErr := io.Copy(io.MultiWriter(file, hash), io.LimitReader(response.Body, pack.Length+1))
|
|
244
|
+
syncErr := file.Sync()
|
|
245
|
+
closeErr := file.Close()
|
|
246
|
+
if copyErr != nil || syncErr != nil || closeErr != nil {
|
|
247
|
+
return errors.Join(copyErr, syncErr, closeErr)
|
|
248
|
+
}
|
|
249
|
+
receipt.CompressedBytes += count
|
|
250
|
+
if count != pack.Length || hex.EncodeToString(hash.Sum(nil)) != pack.SHA256 {
|
|
251
|
+
return fmt.Errorf("artifact pack %s has wrong compressed length or digest", pack.Name)
|
|
252
|
+
}
|
|
253
|
+
return nil
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
func extractPack(store *artifactStore, pack packageartifacts.Pack, archive, stage string, actions []packageartifacts.Action, receipt *preparationReceipt) error {
|
|
257
|
+
file, err := store.root.Open(archive)
|
|
258
|
+
if err != nil {
|
|
259
|
+
return err
|
|
260
|
+
}
|
|
261
|
+
defer file.Close()
|
|
262
|
+
info, err := file.Stat()
|
|
263
|
+
if err != nil || !info.Mode().IsRegular() || info.Size() != pack.Length {
|
|
264
|
+
return fmt.Errorf("artifact pack %s changed before extraction", pack.Name)
|
|
265
|
+
}
|
|
266
|
+
reader, err := zip.NewReader(file, info.Size())
|
|
267
|
+
if err != nil {
|
|
268
|
+
return fmt.Errorf("artifact pack %s is not a ZIP: %w", pack.Name, err)
|
|
269
|
+
}
|
|
270
|
+
expected := make(map[string]int64)
|
|
271
|
+
for _, action := range actions {
|
|
272
|
+
if action.Pack == pack.Name {
|
|
273
|
+
expected[action.OutputID] = action.BodyLength
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if len(reader.File) != pack.FileCount || len(expected) != pack.FileCount {
|
|
277
|
+
return fmt.Errorf("artifact pack %s has wrong file count", pack.Name)
|
|
278
|
+
}
|
|
279
|
+
var total int64
|
|
280
|
+
seen := make(map[string]bool, len(expected))
|
|
281
|
+
for _, entry := range reader.File {
|
|
282
|
+
if !strings.HasPrefix(entry.Name, "bodies/") {
|
|
283
|
+
return fmt.Errorf("artifact pack %s contains an unexpected path", pack.Name)
|
|
284
|
+
}
|
|
285
|
+
outputID := strings.TrimPrefix(entry.Name, "bodies/")
|
|
286
|
+
length, exists := expected[outputID]
|
|
287
|
+
if !exists || seen[outputID] || entry.UncompressedSize64 != uint64(length) || total > pack.DecodedBytes-length {
|
|
288
|
+
return fmt.Errorf("artifact pack %s contains an unexpected body", pack.Name)
|
|
289
|
+
}
|
|
290
|
+
seen[outputID] = true
|
|
291
|
+
compressed, err := entry.Open()
|
|
292
|
+
if err != nil {
|
|
293
|
+
return err
|
|
294
|
+
}
|
|
295
|
+
destination := filepath.ToSlash(filepath.Join(stage, "bodies", outputID))
|
|
296
|
+
file, err := store.root.OpenFile(destination, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
|
|
297
|
+
if err != nil {
|
|
298
|
+
compressed.Close()
|
|
299
|
+
return err
|
|
300
|
+
}
|
|
301
|
+
hash := sha256.New()
|
|
302
|
+
count, copyErr := io.Copy(io.MultiWriter(file, hash), io.LimitReader(compressed, length+1))
|
|
303
|
+
closeInputErr := compressed.Close()
|
|
304
|
+
syncErr := file.Sync()
|
|
305
|
+
closeOutputErr := file.Close()
|
|
306
|
+
if copyErr != nil || closeInputErr != nil || syncErr != nil || closeOutputErr != nil {
|
|
307
|
+
return errors.Join(copyErr, closeInputErr, syncErr, closeOutputErr)
|
|
308
|
+
}
|
|
309
|
+
if count != length || hex.EncodeToString(hash.Sum(nil)) != outputID {
|
|
310
|
+
return fmt.Errorf("artifact pack %s contains a corrupt body", pack.Name)
|
|
311
|
+
}
|
|
312
|
+
total += count
|
|
313
|
+
}
|
|
314
|
+
if total != pack.DecodedBytes {
|
|
315
|
+
return fmt.Errorf("artifact pack %s has wrong decoded byte count", pack.Name)
|
|
316
|
+
}
|
|
317
|
+
receipt.DecodedBytes += total
|
|
318
|
+
return nil
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
func safeRedirect(target *url.URL) bool {
|
|
322
|
+
if target.User != nil || target.Fragment != "" {
|
|
323
|
+
return false
|
|
324
|
+
}
|
|
325
|
+
if target.Scheme == "https" {
|
|
326
|
+
return true
|
|
327
|
+
}
|
|
328
|
+
address := net.ParseIP(target.Hostname())
|
|
329
|
+
return target.Scheme == "http" && address != nil && address.IsLoopback()
|
|
330
|
+
}
|