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
package/driver/rewrite.go
CHANGED
|
@@ -157,10 +157,13 @@ func (p *Program) EmitAll(rs *RewriteSet, writeFile shimcompiler.WriteFile) (*sh
|
|
|
157
157
|
// is the standard place to carry per-file cursors or an output map), so ttsc
|
|
158
158
|
// owns the serialization rather than pushing goroutine-safety onto every
|
|
159
159
|
// plugin author. See the emit-concurrency contract in the plugin docs.
|
|
160
|
+
// A watch-owned writer may leave an already identical file in place; that
|
|
161
|
+
// successful output remains in EmittedFiles. Rejected outDir paths are marked
|
|
162
|
+
// skipped and do not appear there.
|
|
160
163
|
//
|
|
161
164
|
// @evidence contracts/common.md#principled-implementation Native emission honors linked program hooks, compiler incremental-output policy, and output containment while omitting only ttsc text rewrites.
|
|
162
165
|
// @evidence contracts/common.md#clear-and-simple-design One serialized writer callback wraps the compiler emit owner and shared diagnostic conversion.
|
|
163
|
-
// @evidence contracts/common.md#prohibited-implementation-shortcuts Compiler emission is not patched or replaced with expected-output text;
|
|
166
|
+
// @evidence contracts/common.md#prohibited-implementation-shortcuts Compiler emission is not patched or replaced with expected-output text; rejected outDir writes are marked skipped, while identical existing watch outputs remain successful emitted files.
|
|
164
167
|
// @evidence contracts/common.md#meaningful-documentation Native prose distinguishes raw output and guaranteed callback serialization following the documentation skill.
|
|
165
168
|
// @evidence contracts/portability.md#os-neutral-implementation Native containment resolves compiler paths with its case policy, and DefaultWriteFile uses native filesystem APIs.
|
|
166
169
|
// @evidence contracts/performance.md#efficient-algorithms Native generation processes the current program and emitted text; serialized callbacks add lexical path-containment and default filesystem or arbitrary caller-writer costs. Result diagnostics require conversion. Native threading policy remains selected by the loaded program, with no measured claim that callback serialization is inexpensive.
|
|
@@ -185,8 +188,8 @@ func (p *Program) EmitAllRaw(writeFile shimcompiler.WriteFile) (*shimcompiler.Em
|
|
|
185
188
|
wfMu.Lock()
|
|
186
189
|
defer wfMu.Unlock()
|
|
187
190
|
if p.outputEscapesOutDir(fileName) {
|
|
188
|
-
// Marking
|
|
189
|
-
//
|
|
191
|
+
// Marking a rejected path skipped keeps it out of EmittedFiles. An
|
|
192
|
+
// existing identical output is still an emitted result and stays listed.
|
|
190
193
|
if data != nil {
|
|
191
194
|
data.SkippedDtsWrite = true
|
|
192
195
|
}
|
|
@@ -243,8 +246,8 @@ func (p *Program) emit(rs *RewriteSet, target *ast.SourceFile, writeFile shimcom
|
|
|
243
246
|
wfMu.Lock()
|
|
244
247
|
defer wfMu.Unlock()
|
|
245
248
|
if p.outputEscapesOutDir(fileName) {
|
|
246
|
-
// See EmitAllRaw:
|
|
247
|
-
//
|
|
249
|
+
// See EmitAllRaw: rejected paths are absent from EmittedFiles, while an
|
|
250
|
+
// existing identical watch output remains a successful result.
|
|
248
251
|
if data != nil {
|
|
249
252
|
data.SkippedDtsWrite = true
|
|
250
253
|
}
|
|
@@ -316,19 +319,25 @@ func (p *Program) emitsBuildInfo() bool {
|
|
|
316
319
|
}
|
|
317
320
|
|
|
318
321
|
// DefaultWriteFile is the default disk writer used when EmitAll's caller does not
|
|
319
|
-
// supply a custom WriteFile callback. Existing files are truncated; a
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
+
// supply a custom WriteFile callback. Existing files are normally truncated; a
|
|
323
|
+
// watch child setting TTSC_WATCH_WRITE_IF_CHANGED=1 preserves byte-identical
|
|
324
|
+
// regular output files instead. A failed write can leave partial output.
|
|
325
|
+
// Requested creation modes are subject to the native platform and umask, and
|
|
326
|
+
// do not replace existing file permissions.
|
|
322
327
|
//
|
|
323
|
-
// @evidence contracts/common.md#principled-implementation
|
|
324
|
-
// @evidence contracts/common.md#clear-and-simple-design One
|
|
325
|
-
// @evidence contracts/common.md#prohibited-implementation-shortcuts
|
|
326
|
-
// @evidence contracts/common.md#meaningful-documentation Native prose
|
|
327
|
-
// @evidence contracts/portability.md#os-neutral-implementation filepath.Dir, os.MkdirAll
|
|
328
|
-
// @evidence contracts/performance.md#efficient-algorithms
|
|
329
|
-
// @evidenceExclude contracts/performance.md#reuse-equivalent-work Each emit write is requested independently; this writer owns no reuse coordinator.
|
|
330
|
-
// @evidence contracts/performance.md#bound-retention-and-release-resources
|
|
328
|
+
// @evidence contracts/common.md#principled-implementation The watch-only opt-in compares an existing writable regular output before deciding whether native writing is needed; all other writes use the original directory and file operations.
|
|
329
|
+
// @evidence contracts/common.md#clear-and-simple-design One opt-in decision selects the unchanged-output check or the original writer.
|
|
330
|
+
// @evidence contracts/common.md#prohibited-implementation-shortcuts Native file identity and bytes decide whether to skip; unknown state falls back to the original writer and its errors.
|
|
331
|
+
// @evidence contracts/common.md#meaningful-documentation Native prose states normal truncation, watch opt-in, partial failure and permission behavior.
|
|
332
|
+
// @evidence contracts/portability.md#os-neutral-implementation filepath.Dir, os.MkdirAll and os.WriteFile preserve native path and permission behavior; the optional check uses native file identity rather than path spelling.
|
|
333
|
+
// @evidence contracts/performance.md#efficient-algorithms Normal writes retain one directory creation and file write. Watch writes add metadata and streamed comparison of up to the existing output size, then write only when content differs or safe comparison fails; no prior-output cache is retained.
|
|
334
|
+
// @evidenceExclude contracts/performance.md#reuse-equivalent-work Each emit write is requested independently; this writer owns no cross-emit reuse coordinator.
|
|
335
|
+
// @evidence contracts/performance.md#bound-retention-and-release-resources The optional comparison closes its one read/write handle before return or fallback; os.WriteFile owns and closes the later write handle. No output buffer or handle survives this call, while created directories and partial or completed files remain on disk.
|
|
331
336
|
func DefaultWriteFile(fileName, text string) error {
|
|
337
|
+
return writeFileWithWatchMode(fileName, text, os.Getenv("TTSC_WATCH_WRITE_IF_CHANGED") == "1")
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
func defaultWriteFile(fileName, text string) error {
|
|
332
341
|
if dir := filepath.Dir(fileName); dir != "" {
|
|
333
342
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
334
343
|
return err
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
package driver
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"io"
|
|
6
|
+
"os"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// writeFileWithWatchMode preserves an unchanged regular output only for a
|
|
10
|
+
// watch-owned write. A failed comparison does not replace the default writer's
|
|
11
|
+
// errors or its behavior for symlinks, special files and absent parents.
|
|
12
|
+
func writeFileWithWatchMode(fileName, text string, watch bool) error {
|
|
13
|
+
if watch && unchangedRegularOutput(fileName, text) {
|
|
14
|
+
return nil
|
|
15
|
+
}
|
|
16
|
+
return defaultWriteFile(fileName, text)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// unchangedRegularOutput compares through a read/write descriptor so a file
|
|
20
|
+
// that the normal writer cannot open for writing is never treated as success.
|
|
21
|
+
// Identity and metadata checks reject a file replaced during the comparison;
|
|
22
|
+
// a later independent filesystem mutation remains a caller-visible race.
|
|
23
|
+
func unchangedRegularOutput(fileName, text string) bool {
|
|
24
|
+
before, err := os.Lstat(fileName)
|
|
25
|
+
if err != nil || !before.Mode().IsRegular() || before.Size() != int64(len(text)) {
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
file, err := os.OpenFile(fileName, os.O_RDWR, 0)
|
|
29
|
+
if err != nil {
|
|
30
|
+
return false
|
|
31
|
+
}
|
|
32
|
+
same := matchesOpenRegularOutput(file, fileName, before, text)
|
|
33
|
+
if err := file.Close(); err != nil {
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
return same
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func matchesOpenRegularOutput(file *os.File, fileName string, before os.FileInfo, text string) bool {
|
|
40
|
+
opened, err := file.Stat()
|
|
41
|
+
if err != nil || !opened.Mode().IsRegular() || !os.SameFile(before, opened) || opened.Size() != int64(len(text)) {
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
var buffer [32 * 1024]byte
|
|
45
|
+
for offset := 0; offset < len(text); {
|
|
46
|
+
count := min(len(buffer), len(text)-offset)
|
|
47
|
+
read, err := io.ReadFull(file, buffer[:count])
|
|
48
|
+
if err != nil || !bytes.Equal(buffer[:read], []byte(text[offset:offset+read])) {
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
offset += read
|
|
52
|
+
}
|
|
53
|
+
if _, err := file.Read(buffer[:1]); err != io.EOF {
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
afterOpen, err := file.Stat()
|
|
57
|
+
if err != nil {
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
afterPath, err := os.Lstat(fileName)
|
|
61
|
+
if err != nil {
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
return afterPath.Mode().IsRegular() &&
|
|
65
|
+
os.SameFile(opened, afterOpen) && os.SameFile(opened, afterPath) &&
|
|
66
|
+
afterOpen.Mode() == opened.Mode() && afterPath.Mode() == opened.Mode() &&
|
|
67
|
+
afterOpen.Size() == opened.Size() && afterPath.Size() == opened.Size() &&
|
|
68
|
+
afterOpen.ModTime().Equal(opened.ModTime()) && afterPath.ModTime().Equal(opened.ModTime())
|
|
69
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
package driver
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"testing"
|
|
7
|
+
"time"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// TestWriteFileWithWatchMode verifies the watch writer's disk effects and
|
|
11
|
+
// fallback against the ordinary writer.
|
|
12
|
+
//
|
|
13
|
+
// Verifies that only an existing, byte-identical regular file is preserved.
|
|
14
|
+
// 1. Write missing and changed outputs in watch mode and read their bytes.
|
|
15
|
+
// 2. Mark identical outputs with an old timestamp and compare watch and normal writes.
|
|
16
|
+
// 3. Exercise blocked parents, read-only outputs and a symlink against normal writes.
|
|
17
|
+
//
|
|
18
|
+
// @evidence contracts/testing.md#behavioral-verification The direct disk writer creates and changes real files, preserves an unchanged watch output's timestamp and mode, and still reports native write failures.
|
|
19
|
+
// @evidence contracts/testing.md#independent-expectations Authored literal bytes and a fixed old timestamp establish the expected output independently of the writer; normal mode on an equivalent file is the permission/error reference.
|
|
20
|
+
// @evidence contracts/testing.md#distinguishing-cases Missing, empty, unchanged, changed same-size, blocked-parent, read-only and symlink outputs separate the skip predicate from writes and error fallback. Compiler emitted-file accounting belongs to its owning host test.
|
|
21
|
+
// @evidence contracts/testing.md#execution-ownership This Go unit calls the driver writer with explicit watch mode on temporary native files; no compiler, child host or process environment mutation is required.
|
|
22
|
+
func TestWriteFileWithWatchMode(t *testing.T) {
|
|
23
|
+
old := time.Date(2001, time.January, 1, 0, 0, 0, 0, time.UTC)
|
|
24
|
+
|
|
25
|
+
t.Run("missing and changed", func(t *testing.T) {
|
|
26
|
+
file := filepath.Join(t.TempDir(), "nested", "out.js")
|
|
27
|
+
if err := writeFileWithWatchMode(file, "first", true); err != nil {
|
|
28
|
+
t.Fatal(err)
|
|
29
|
+
}
|
|
30
|
+
if err := writeFileWithWatchMode(file, "other", true); err != nil {
|
|
31
|
+
t.Fatal(err)
|
|
32
|
+
}
|
|
33
|
+
data, err := os.ReadFile(file)
|
|
34
|
+
if err != nil || string(data) != "other" {
|
|
35
|
+
t.Fatalf("changed output = %q, %v", data, err)
|
|
36
|
+
}
|
|
37
|
+
if err := writeFileWithWatchMode(file, "", true); err != nil {
|
|
38
|
+
t.Fatal(err)
|
|
39
|
+
}
|
|
40
|
+
data, err = os.ReadFile(file)
|
|
41
|
+
if err != nil || len(data) != 0 {
|
|
42
|
+
t.Fatalf("empty output = %q, %v", data, err)
|
|
43
|
+
}
|
|
44
|
+
if err := os.Chtimes(file, old, old); err != nil {
|
|
45
|
+
t.Fatal(err)
|
|
46
|
+
}
|
|
47
|
+
if err := writeFileWithWatchMode(file, "", true); err != nil {
|
|
48
|
+
t.Fatal(err)
|
|
49
|
+
}
|
|
50
|
+
info, err := os.Stat(file)
|
|
51
|
+
if err != nil || !info.ModTime().Equal(old) {
|
|
52
|
+
t.Fatalf("empty output was rewritten: %v, %v", info, err)
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
t.Run("identical watch and ordinary output", func(t *testing.T) {
|
|
57
|
+
root := t.TempDir()
|
|
58
|
+
watch := filepath.Join(root, "watch.js")
|
|
59
|
+
normal := filepath.Join(root, "normal.js")
|
|
60
|
+
for _, file := range []string{watch, normal} {
|
|
61
|
+
if err := os.WriteFile(file, []byte("same"), 0o600); err != nil {
|
|
62
|
+
t.Fatal(err)
|
|
63
|
+
}
|
|
64
|
+
if err := os.Chtimes(file, old, old); err != nil {
|
|
65
|
+
t.Fatal(err)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
before, err := os.Stat(watch)
|
|
69
|
+
if err != nil {
|
|
70
|
+
t.Fatal(err)
|
|
71
|
+
}
|
|
72
|
+
if err := writeFileWithWatchMode(watch, "same", true); err != nil {
|
|
73
|
+
t.Fatal(err)
|
|
74
|
+
}
|
|
75
|
+
after, err := os.Stat(watch)
|
|
76
|
+
if err != nil {
|
|
77
|
+
t.Fatal(err)
|
|
78
|
+
}
|
|
79
|
+
if !after.ModTime().Equal(before.ModTime()) || after.Mode() != before.Mode() {
|
|
80
|
+
t.Fatalf("unchanged watch metadata changed: before=%v after=%v", before, after)
|
|
81
|
+
}
|
|
82
|
+
if err := writeFileWithWatchMode(normal, "same", false); err != nil {
|
|
83
|
+
t.Fatal(err)
|
|
84
|
+
}
|
|
85
|
+
normalAfter, err := os.Stat(normal)
|
|
86
|
+
if err != nil {
|
|
87
|
+
t.Fatal(err)
|
|
88
|
+
}
|
|
89
|
+
if normalAfter.ModTime().Equal(old) {
|
|
90
|
+
t.Fatal("ordinary identical output was not rewritten")
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
t.Run("blocked parent", func(t *testing.T) {
|
|
95
|
+
parent := filepath.Join(t.TempDir(), "parent")
|
|
96
|
+
if err := os.WriteFile(parent, []byte("file"), 0o600); err != nil {
|
|
97
|
+
t.Fatal(err)
|
|
98
|
+
}
|
|
99
|
+
if err := writeFileWithWatchMode(filepath.Join(parent, "out.js"), "x", true); err == nil {
|
|
100
|
+
t.Fatal("watch writer accepted a blocked parent")
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
t.Run("read-only output", func(t *testing.T) {
|
|
105
|
+
root := t.TempDir()
|
|
106
|
+
watch := filepath.Join(root, "watch.js")
|
|
107
|
+
normal := filepath.Join(root, "normal.js")
|
|
108
|
+
for _, file := range []string{watch, normal} {
|
|
109
|
+
if err := os.WriteFile(file, []byte("same"), 0o600); err != nil {
|
|
110
|
+
t.Fatal(err)
|
|
111
|
+
}
|
|
112
|
+
if err := os.Chmod(file, 0o400); err != nil {
|
|
113
|
+
t.Fatal(err)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
watchErr := writeFileWithWatchMode(watch, "same", true)
|
|
117
|
+
normalErr := writeFileWithWatchMode(normal, "same", false)
|
|
118
|
+
if (watchErr == nil) != (normalErr == nil) {
|
|
119
|
+
t.Fatalf("read-only write error differs: watch=%v normal=%v", watchErr, normalErr)
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
t.Run("symlink output", func(t *testing.T) {
|
|
124
|
+
root := t.TempDir()
|
|
125
|
+
target := filepath.Join(root, "target.js")
|
|
126
|
+
link := filepath.Join(root, "link.js")
|
|
127
|
+
if err := os.WriteFile(target, []byte("same"), 0o600); err != nil {
|
|
128
|
+
t.Fatal(err)
|
|
129
|
+
}
|
|
130
|
+
if err := os.Symlink(target, link); err != nil {
|
|
131
|
+
t.Skipf("symlink unavailable: %v", err)
|
|
132
|
+
}
|
|
133
|
+
if err := os.Chtimes(target, old, old); err != nil {
|
|
134
|
+
t.Fatal(err)
|
|
135
|
+
}
|
|
136
|
+
if err := writeFileWithWatchMode(link, "same", true); err != nil {
|
|
137
|
+
t.Fatal(err)
|
|
138
|
+
}
|
|
139
|
+
info, err := os.Lstat(link)
|
|
140
|
+
if err != nil || info.Mode()&os.ModeSymlink == 0 {
|
|
141
|
+
t.Fatalf("output symlink was replaced: %v, %v", info, err)
|
|
142
|
+
}
|
|
143
|
+
targetInfo, err := os.Stat(target)
|
|
144
|
+
if err != nil || targetInfo.ModTime().Equal(old) {
|
|
145
|
+
t.Fatalf("symlink target was not rewritten: %v, %v", targetInfo, err)
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// Package packageartifacts validates published Go cache actions before they enter a local base.
|
|
2
|
+
package packageartifacts
|
|
3
|
+
|
|
4
|
+
import (
|
|
5
|
+
"bytes"
|
|
6
|
+
"crypto/sha256"
|
|
7
|
+
"encoding/hex"
|
|
8
|
+
"encoding/json"
|
|
9
|
+
"errors"
|
|
10
|
+
"fmt"
|
|
11
|
+
"io"
|
|
12
|
+
"net"
|
|
13
|
+
"net/url"
|
|
14
|
+
"strings"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
const MaxManifestBytes = 16 << 20
|
|
18
|
+
|
|
19
|
+
const MaxPackBytes = 512 << 20
|
|
20
|
+
|
|
21
|
+
const MaxPackDecodedBytes = 4 << 30
|
|
22
|
+
|
|
23
|
+
// Manifest is a candidate catalog. Only Go's full ActionID decides a cache hit.
|
|
24
|
+
// Its exact serialized bytes must match a digest carried by the trusted release package.
|
|
25
|
+
//
|
|
26
|
+
// @evidence contracts/common.md#principled-implementation Release provenance, Go selection, transport packs and complete action associations are kept together so admission cannot use an unauthenticated partial catalog.
|
|
27
|
+
// @evidence contracts/common.md#clear-and-simple-design One versioned value is decoded and validated before any pack or action is exposed.
|
|
28
|
+
// @evidence contracts/common.md#prohibited-implementation-shortcuts The graph digest diagnoses a build; it never substitutes for Go's action key or a release-pinned digest.
|
|
29
|
+
// @evidence contracts/common.md#meaningful-documentation The comment identifies candidate-only selection and the external trust prerequisite.
|
|
30
|
+
//
|
|
31
|
+
// @evidenceExclude contracts/portability.md#os-neutral-implementation This JSON value has no native path or process operation.
|
|
32
|
+
// @evidenceExclude contracts/performance.md#efficient-algorithms The type chooses no algorithm; parsing and traversal belong to ParseTrustedManifest.
|
|
33
|
+
// @evidenceExclude contracts/performance.md#reuse-equivalent-work The value owns no retained computation.
|
|
34
|
+
// @evidenceExclude contracts/performance.md#bound-retention-and-release-resources The value owns no external resource.
|
|
35
|
+
type Manifest struct {
|
|
36
|
+
Schema int `json:"schema"`
|
|
37
|
+
Release string `json:"release"`
|
|
38
|
+
SourceCommit string `json:"sourceCommit"`
|
|
39
|
+
Tuple Tuple `json:"tuple"`
|
|
40
|
+
GraphSHA256 string `json:"graphSHA256"`
|
|
41
|
+
Packs []Pack `json:"packs"`
|
|
42
|
+
Actions []Action `json:"actions"`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Tuple selects candidate packs, while Go validates actual package actions.
|
|
46
|
+
// Initial publisher coverage is limited to an observed effective CGO_ENABLED=0.
|
|
47
|
+
//
|
|
48
|
+
// @evidence contracts/common.md#principled-implementation The recorded Go tool, target, features and build policy choose a possible pack without claiming that equal tuple fields imply equal ActionIDs.
|
|
49
|
+
// @evidence contracts/common.md#clear-and-simple-design Selection fields form one value and leave actual cache matching to Go.
|
|
50
|
+
// @evidence contracts/common.md#prohibited-implementation-shortcuts The CGO field records the observed setting and cannot be rewritten to manufacture coverage.
|
|
51
|
+
// @evidence contracts/common.md#meaningful-documentation The comment states the tuple's limited authority and initial native eligibility.
|
|
52
|
+
//
|
|
53
|
+
// @evidenceExclude contracts/portability.md#os-neutral-implementation These are Go-reported target strings, not native filesystem paths or process calls.
|
|
54
|
+
// @evidenceExclude contracts/performance.md#efficient-algorithms The type chooses no algorithm.
|
|
55
|
+
// @evidenceExclude contracts/performance.md#reuse-equivalent-work The type retains no result across requests.
|
|
56
|
+
// @evidenceExclude contracts/performance.md#bound-retention-and-release-resources The type owns no handle or task.
|
|
57
|
+
type Tuple struct {
|
|
58
|
+
GoVersion string `json:"goVersion"`
|
|
59
|
+
GoToolSHA256 string `json:"goToolSHA256"`
|
|
60
|
+
GOOS string `json:"goos"`
|
|
61
|
+
GOARCH string `json:"goarch"`
|
|
62
|
+
Architecture map[string]string `json:"architecture"`
|
|
63
|
+
CGOEnabled string `json:"cgoEnabled"`
|
|
64
|
+
GoExperiment string `json:"goExperiment"`
|
|
65
|
+
BuildFlags []string `json:"buildFlags"`
|
|
66
|
+
BuildTags []string `json:"buildTags"`
|
|
67
|
+
ToolIDsSHA256 string `json:"toolIDsSHA256"`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Pack describes one immutable compressed transport object and its decoded limit.
|
|
71
|
+
//
|
|
72
|
+
// @evidence contracts/common.md#principled-implementation A transport digest and compressed/decoded counts are separate so admission can bound both transfer and extracted bodies.
|
|
73
|
+
// @evidence contracts/common.md#clear-and-simple-design One pack record carries the bytes and count limits needed by its reader.
|
|
74
|
+
// @evidence contracts/common.md#prohibited-implementation-shortcuts A pack hash from the pack itself is not producer authority; the enclosing Manifest requires a separately trusted digest.
|
|
75
|
+
// @evidence contracts/common.md#meaningful-documentation The comment names the immutable object and decoded limit; fields carry their JSON units in names.
|
|
76
|
+
//
|
|
77
|
+
// @evidenceExclude contracts/portability.md#os-neutral-implementation The name is a transport identifier, not an interpreted native path.
|
|
78
|
+
// @evidenceExclude contracts/performance.md#efficient-algorithms The record chooses no computation strategy.
|
|
79
|
+
// @evidenceExclude contracts/performance.md#reuse-equivalent-work The record retains no cross-request computation.
|
|
80
|
+
// @evidenceExclude contracts/performance.md#bound-retention-and-release-resources The record owns no downloaded bytes or handle.
|
|
81
|
+
type Pack struct {
|
|
82
|
+
Name string `json:"name"`
|
|
83
|
+
URL string `json:"url"`
|
|
84
|
+
SHA256 string `json:"sha256"`
|
|
85
|
+
Length int64 `json:"length"`
|
|
86
|
+
DecodedBytes int64 `json:"decodedBytes"`
|
|
87
|
+
FileCount int `json:"fileCount"`
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Action associates a complete Go cache key with the exact body Go produced.
|
|
91
|
+
// Evidence is diagnostic; no package path, graph digest or displayed build ID replaces ActionID.
|
|
92
|
+
//
|
|
93
|
+
// @evidence contracts/common.md#principled-implementation The full Go action and output identities own lookup; package/module/source/command/dependency fields explain a captured association without changing it.
|
|
94
|
+
// @evidence contracts/common.md#clear-and-simple-design Archive and opaque bodies share one association because both use the same Go cache protocol.
|
|
95
|
+
// @evidence contracts/common.md#prohibited-implementation-shortcuts The package annotation never synthesizes a key, and an opaque record cannot be silently interpreted as an archive.
|
|
96
|
+
// @evidence contracts/common.md#meaningful-documentation The comment distinguishes cache authority from diagnostic evidence.
|
|
97
|
+
//
|
|
98
|
+
// @evidenceExclude contracts/portability.md#os-neutral-implementation Paths are Go package/module identifiers and full digests, not native filesystem paths.
|
|
99
|
+
// @evidenceExclude contracts/performance.md#efficient-algorithms The record selects no algorithm.
|
|
100
|
+
// @evidenceExclude contracts/performance.md#reuse-equivalent-work The record retains no computation across calls.
|
|
101
|
+
// @evidenceExclude contracts/performance.md#bound-retention-and-release-resources The record owns no body or handle.
|
|
102
|
+
type Action struct {
|
|
103
|
+
ActionID string `json:"actionID"`
|
|
104
|
+
OutputID string `json:"outputID"`
|
|
105
|
+
BodySHA256 string `json:"bodySHA256"`
|
|
106
|
+
BodyLength int64 `json:"bodyLength"`
|
|
107
|
+
Pack string `json:"pack"`
|
|
108
|
+
Kind string `json:"kind"`
|
|
109
|
+
PackagePath string `json:"packagePath"`
|
|
110
|
+
ModulePath string `json:"modulePath"`
|
|
111
|
+
ModuleVersion string `json:"moduleVersion"`
|
|
112
|
+
ModuleRole string `json:"moduleRole"`
|
|
113
|
+
SelectedSourceSHA string `json:"selectedSourceSHA256"`
|
|
114
|
+
CommandSHA256 string `json:"commandSHA256"`
|
|
115
|
+
DirectInputs []string `json:"directInputs"`
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ParseTrustedManifest checks the raw release-pinned digest before decoding any mapping.
|
|
119
|
+
// The caller must obtain expectedSHA256 from authenticated release metadata, never from raw.
|
|
120
|
+
//
|
|
121
|
+
// @evidence contracts/common.md#principled-implementation SHA-256 of exact bounded bytes is compared with an external trusted digest before strict schema and full 32-byte action/output validation; Go alone computes the keys.
|
|
122
|
+
// @evidence contracts/common.md#clear-and-simple-design One decoder owns producer authentication, schema and association checks before returning a usable manifest.
|
|
123
|
+
// @evidence contracts/common.md#prohibited-implementation-shortcuts A digest supplied by the same untrusted manifest is never accepted as authority; duplicate conflicting actions fail rather than choosing a convenient output.
|
|
124
|
+
// @evidence contracts/common.md#meaningful-documentation The comment states the trust source and that the raw bytes, not a reserialized form, are authenticated.
|
|
125
|
+
//
|
|
126
|
+
// @evidenceExclude contracts/portability.md#os-neutral-implementation This byte decoder does no native path or process operation.
|
|
127
|
+
// @evidence contracts/performance.md#efficient-algorithms It hashes at most 16 MiB of input once, then decodes and traverses at most 64 packs and 100000 actions; maps give expected constant-time identity lookup with memory proportional to those populations.
|
|
128
|
+
// @evidenceExclude contracts/performance.md#reuse-equivalent-work Release bytes are checked for each call; this function retains no result or in-flight work.
|
|
129
|
+
// @evidenceExclude contracts/performance.md#bound-retention-and-release-resources Only bounded call-local bytes and maps are retained; the caller owns raw input and returned manifest lifetime.
|
|
130
|
+
func ParseTrustedManifest(raw []byte, expectedSHA256 string) (Manifest, error) {
|
|
131
|
+
var manifest Manifest
|
|
132
|
+
if len(raw) == 0 || len(raw) > MaxManifestBytes {
|
|
133
|
+
return manifest, fmt.Errorf("artifact manifest length %d is outside (0,%d]", len(raw), MaxManifestBytes)
|
|
134
|
+
}
|
|
135
|
+
if !validSHA256(expectedSHA256) {
|
|
136
|
+
return manifest, errors.New("artifact manifest has no valid trusted digest")
|
|
137
|
+
}
|
|
138
|
+
digest := sha256.Sum256(raw)
|
|
139
|
+
if hex.EncodeToString(digest[:]) != expectedSHA256 {
|
|
140
|
+
return manifest, errors.New("artifact manifest differs from trusted release digest")
|
|
141
|
+
}
|
|
142
|
+
decoder := json.NewDecoder(bytes.NewReader(raw))
|
|
143
|
+
decoder.DisallowUnknownFields()
|
|
144
|
+
if err := decoder.Decode(&manifest); err != nil {
|
|
145
|
+
return Manifest{}, fmt.Errorf("decode artifact manifest: %w", err)
|
|
146
|
+
}
|
|
147
|
+
var trailing any
|
|
148
|
+
if err := decoder.Decode(&trailing); err != io.EOF {
|
|
149
|
+
return Manifest{}, errors.New("artifact manifest contains trailing data")
|
|
150
|
+
}
|
|
151
|
+
if err := manifest.validate(); err != nil {
|
|
152
|
+
return Manifest{}, err
|
|
153
|
+
}
|
|
154
|
+
return manifest, nil
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
func (manifest Manifest) validate() error {
|
|
158
|
+
if manifest.Schema != 1 {
|
|
159
|
+
return fmt.Errorf("unsupported artifact manifest schema %d", manifest.Schema)
|
|
160
|
+
}
|
|
161
|
+
if manifest.Release == "" || !validCommit(manifest.SourceCommit) || !validSHA256(manifest.GraphSHA256) {
|
|
162
|
+
return errors.New("artifact manifest has incomplete release provenance")
|
|
163
|
+
}
|
|
164
|
+
tuple := manifest.Tuple
|
|
165
|
+
if tuple.GoVersion == "" || tuple.GOOS == "" || tuple.GOARCH == "" || tuple.CGOEnabled != "0" || !validSHA256(tuple.GoToolSHA256) || !validSHA256(tuple.ToolIDsSHA256) || len(tuple.BuildFlags) == 0 {
|
|
166
|
+
return errors.New("artifact manifest has an unsupported or incomplete Go tuple")
|
|
167
|
+
}
|
|
168
|
+
if len(manifest.Packs) == 0 || len(manifest.Packs) > 64 || len(manifest.Actions) == 0 || len(manifest.Actions) > 100000 {
|
|
169
|
+
return errors.New("artifact manifest exceeds pack or action count policy")
|
|
170
|
+
}
|
|
171
|
+
packs := make(map[string]Pack, len(manifest.Packs))
|
|
172
|
+
for _, pack := range manifest.Packs {
|
|
173
|
+
if !validName(pack.Name) || !validPackURL(pack.URL) || !validSHA256(pack.SHA256) || pack.Length <= 0 || pack.Length > MaxPackBytes || pack.DecodedBytes < 0 || pack.DecodedBytes > MaxPackDecodedBytes || pack.FileCount <= 0 {
|
|
174
|
+
return fmt.Errorf("invalid artifact pack %q", pack.Name)
|
|
175
|
+
}
|
|
176
|
+
if _, exists := packs[pack.Name]; exists {
|
|
177
|
+
return fmt.Errorf("duplicate artifact pack %q", pack.Name)
|
|
178
|
+
}
|
|
179
|
+
packs[pack.Name] = pack
|
|
180
|
+
}
|
|
181
|
+
actions := make(map[string]Action, len(manifest.Actions))
|
|
182
|
+
outputs := make(map[string]int64, len(manifest.Actions))
|
|
183
|
+
outputPacks := make(map[string]string, len(manifest.Actions))
|
|
184
|
+
packOutputs := make(map[string]map[string]int64, len(manifest.Packs))
|
|
185
|
+
for _, action := range manifest.Actions {
|
|
186
|
+
if !validSHA256(action.ActionID) || !validSHA256(action.OutputID) || action.BodySHA256 != action.OutputID || action.BodyLength < 0 {
|
|
187
|
+
return fmt.Errorf("invalid full action/output identity %q", action.ActionID)
|
|
188
|
+
}
|
|
189
|
+
if action.BodyLength == 0 && action.OutputID != emptyBodySHA256 {
|
|
190
|
+
return fmt.Errorf("invalid empty body identity for action %s", action.ActionID)
|
|
191
|
+
}
|
|
192
|
+
if action.Kind != "archive" && action.Kind != "opaque" {
|
|
193
|
+
return fmt.Errorf("unsupported cache body kind %q", action.Kind)
|
|
194
|
+
}
|
|
195
|
+
if action.Kind == "archive" && (action.PackagePath == "" || action.ModuleRole == "" || !validSHA256(action.SelectedSourceSHA) || !validSHA256(action.CommandSHA256)) {
|
|
196
|
+
return fmt.Errorf("incomplete package evidence for action %s", action.ActionID)
|
|
197
|
+
}
|
|
198
|
+
if _, exists := packs[action.Pack]; !exists {
|
|
199
|
+
return fmt.Errorf("action %s names unknown pack %q", action.ActionID, action.Pack)
|
|
200
|
+
}
|
|
201
|
+
if previous, exists := actions[action.ActionID]; exists {
|
|
202
|
+
if previous.OutputID != action.OutputID || previous.BodyLength != action.BodyLength {
|
|
203
|
+
return fmt.Errorf("conflicting publisher mapping for action %s", action.ActionID)
|
|
204
|
+
}
|
|
205
|
+
return fmt.Errorf("duplicate publisher action %s", action.ActionID)
|
|
206
|
+
}
|
|
207
|
+
if length, exists := outputs[action.OutputID]; exists && length != action.BodyLength {
|
|
208
|
+
return fmt.Errorf("conflicting body length for output %s", action.OutputID)
|
|
209
|
+
}
|
|
210
|
+
if owner, exists := outputPacks[action.OutputID]; exists && owner != action.Pack {
|
|
211
|
+
return fmt.Errorf("output %s is duplicated across packs", action.OutputID)
|
|
212
|
+
}
|
|
213
|
+
for _, input := range action.DirectInputs {
|
|
214
|
+
if !validSHA256(input) {
|
|
215
|
+
return fmt.Errorf("action %s has a malformed direct input", action.ActionID)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
actions[action.ActionID] = action
|
|
219
|
+
outputs[action.OutputID] = action.BodyLength
|
|
220
|
+
outputPacks[action.OutputID] = action.Pack
|
|
221
|
+
if packOutputs[action.Pack] == nil {
|
|
222
|
+
packOutputs[action.Pack] = make(map[string]int64)
|
|
223
|
+
}
|
|
224
|
+
packOutputs[action.Pack][action.OutputID] = action.BodyLength
|
|
225
|
+
}
|
|
226
|
+
for _, pack := range manifest.Packs {
|
|
227
|
+
var total int64
|
|
228
|
+
for _, length := range packOutputs[pack.Name] {
|
|
229
|
+
if length > pack.DecodedBytes-total {
|
|
230
|
+
return fmt.Errorf("pack %q exceeds its decoded byte count", pack.Name)
|
|
231
|
+
}
|
|
232
|
+
total += length
|
|
233
|
+
}
|
|
234
|
+
if len(packOutputs[pack.Name]) != pack.FileCount || total != pack.DecodedBytes {
|
|
235
|
+
return fmt.Errorf("pack %q has inconsistent decoded counts", pack.Name)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return nil
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const emptyBodySHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
|
242
|
+
|
|
243
|
+
func validSHA256(value string) bool {
|
|
244
|
+
if len(value) != 64 {
|
|
245
|
+
return false
|
|
246
|
+
}
|
|
247
|
+
for _, char := range value {
|
|
248
|
+
if !strings.ContainsRune("0123456789abcdef", char) {
|
|
249
|
+
return false
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return true
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
func validCommit(value string) bool {
|
|
256
|
+
if len(value) != 40 && len(value) != 64 {
|
|
257
|
+
return false
|
|
258
|
+
}
|
|
259
|
+
for _, char := range value {
|
|
260
|
+
if !strings.ContainsRune("0123456789abcdef", char) {
|
|
261
|
+
return false
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return true
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
func validName(value string) bool {
|
|
268
|
+
if value == "" || len(value) > 80 {
|
|
269
|
+
return false
|
|
270
|
+
}
|
|
271
|
+
for _, char := range value {
|
|
272
|
+
if !strings.ContainsRune("abcdefghijklmnopqrstuvwxyz0123456789-", char) {
|
|
273
|
+
return false
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return true
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
func validPackURL(value string) bool {
|
|
280
|
+
parsed, err := url.Parse(value)
|
|
281
|
+
if err != nil || parsed.User != nil || parsed.Host == "" || parsed.Fragment != "" {
|
|
282
|
+
return false
|
|
283
|
+
}
|
|
284
|
+
if parsed.Scheme == "https" {
|
|
285
|
+
return true
|
|
286
|
+
}
|
|
287
|
+
return parsed.Scheme == "http" && net.ParseIP(parsed.Hostname()) != nil && net.ParseIP(parsed.Hostname()).IsLoopback()
|
|
288
|
+
}
|