Prepare Sandwich Hime v1 release candidate source

This commit is contained in:
2026-08-23 16:25:02 -04:00
parent c11552b87a
commit efd5b997ed
64 changed files with 5276 additions and 156 deletions
+138
View File
@@ -0,0 +1,138 @@
// SPDX-License-Identifier: AGPL-3.0-only
package compiler
import (
"bytes"
"context"
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"regexp"
"runtime"
"sort"
"strconv"
"strings"
"testing"
)
var diagnosticCodePattern = regexp.MustCompile(`^HIM[0-9]{4}$`)
func TestV1DiagnosticCodeContract(t *testing.T) {
t.Parallel()
directory := packageDirectory(t)
entries, err := os.ReadDir(directory)
if err != nil {
t.Fatal(err)
}
codes := make(map[string]struct{})
fileSet := token.NewFileSet()
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") || strings.HasSuffix(entry.Name(), "_test.go") {
continue
}
parsed, err := parser.ParseFile(fileSet, filepath.Join(directory, entry.Name()), nil, 0)
if err != nil {
t.Fatalf("parse %s: %v", entry.Name(), err)
}
ast.Inspect(parsed, func(node ast.Node) bool {
literal, ok := node.(*ast.BasicLit)
if !ok || literal.Kind != token.STRING {
return true
}
value, err := strconv.Unquote(literal.Value)
if err == nil && diagnosticCodePattern.MatchString(value) {
codes[value] = struct{}{}
}
return true
})
}
actual := make([]string, 0, len(codes))
for code := range codes {
actual = append(actual, code)
}
sort.Strings(actual)
assertContractFile(t, filepath.Join(directory, "..", "..", "contracts", "diagnostic-codes-v1.txt"), strings.Join(actual, "\n")+"\n")
}
func TestV1GeneratedProvenanceContract(t *testing.T) {
t.Parallel()
compiled, diagnostics := Compile("views/generic.sando", []byte(`<?sando go
package views
func List[T ~string](values []T)
?>
<ul><? for _, value := range values { ?><li><?= value ?></li><? } ?></ul>`))
assertNoErrorDiagnostics(t, diagnostics)
lines := strings.Split(string(compiled.Code), "\n")
if len(lines) < 4 {
t.Fatalf("generated header has %d lines", len(lines))
}
actual := []string{lines[0]}
if !strings.HasPrefix(lines[1], "// himesan:compiler ") {
t.Fatalf("compiler provenance line = %q", lines[1])
}
actual = append(actual, "// himesan:compiler <compiler-version>")
if !strings.HasPrefix(lines[2], "// himesan:runtime-abi ") {
t.Fatalf("runtime provenance line = %q", lines[2])
}
actual = append(actual, "// himesan:runtime-abi <runtime-abi>")
if !regexp.MustCompile(`^// himesan:source-sha256 [0-9a-f]{64}$`).MatchString(lines[3]) {
t.Fatalf("source provenance line = %q", lines[3])
}
actual = append(actual, "// himesan:source-sha256 <lowercase-sha256>")
generated := string(compiled.Code)
if !regexp.MustCompile(`(?m)^var _ = [A-Za-z_][A-Za-z0-9_]*\.ABISandoV1$`).MatchString(generated) {
t.Fatal("generated output is missing the compile-time ABI marker")
}
actual = append(actual, "var _ = <sando-import>.ABISandoV1")
if !regexp.MustCompile(`(?m)^//line [^\r\n]+:[1-9][0-9]*:[1-9][0-9]*$`).MatchString(generated) {
t.Fatal("generated output is missing source mappings")
}
actual = append(actual, "//line <source-path>:<line>:<column>")
assertContractFile(t, filepath.Join(packageDirectory(t), "..", "..", "contracts", "generated-provenance-v1.txt"), strings.Join(actual, "\n")+"\n")
}
func TestV1GenericComponentSignature(t *testing.T) {
t.Parallel()
source := []byte(`<?sando go
package views
func List[T ~string](values []T)
?>
<ul><? for _, value := range values { ?><li><?= value ?></li><? } ?></ul>`)
compiled, diagnostics := Compile("views/list.sando", source)
assertNoErrorDiagnostics(t, diagnostics)
if !bytes.Contains(compiled.Code, []byte("func List[T ~string](values []T)")) {
t.Fatalf("generic signature was not preserved:\n%s", compiled.Code)
}
analyses := AnalyzeSources(context.Background(), []SourceInput{{Path: "views/list.sando", Source: source}})
if len(analyses) != 1 {
t.Fatalf("analysis count = %d, want 1", len(analyses))
}
analysis := analyses[0]
if analysis.TypeParams != "[T ~string]" || analysis.Params != "(values []T)" || analysis.Signature != "func List[T ~string](values []T)" {
t.Fatalf("generic analysis contract = %#v", analysis)
}
}
func packageDirectory(t *testing.T) string {
t.Helper()
_, file, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
return filepath.Dir(file)
}
func assertContractFile(t *testing.T, path, actual string) {
t.Helper()
expected, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
expected = bytes.TrimPrefix(expected, []byte("# SPDX-License-Identifier: AGPL-3.0-only\n\n"))
if string(expected) != actual {
t.Fatalf("contract drift in %s\n--- expected ---\n%s--- actual ---\n%s", path, expected, actual)
}
}
+10 -1
View File
@@ -128,6 +128,9 @@ func TestRCDATACannotBeBypassedByTrustedHTML(t *testing.T) {
view := View{Name: "title", URL: "/", JS: sando.TrustJS(""), HTML: sando.TrustHTML("</textarea><script>bad()</script>")}
if err := sando.Render(context.Background(), &output, Page(view)); err != nil { t.Fatal(err) }
if strings.Contains(output.String(), "</textarea><script>") { t.Fatalf("RCDATA boundary escaped: %s", output.String()) }
output.Reset()
if err := sando.Render(context.Background(), &output, List([]string{"one", "two"})); err != nil { t.Fatal(err) }
if output.String() != "\n<ul><li>one</li><li>two</li></ul>" { t.Fatalf("generic component output: %q", output.String()) }
}
`)
templatePath := filepath.Join(directory, "page.sando")
@@ -141,7 +144,13 @@ func Page(view View)
<script><?= view.JS ?></script>
<textarea><?= view.HTML ?></textarea>
</body></html>`)
result, err := Generate(context.Background(), []string{templatePath})
genericPath := filepath.Join(directory, "list.sando")
mustWrite(t, genericPath, `<?sando go
package generated
func List[T ~string](values []T)
?>
<ul><? for _, value := range values { ?><li><?= value ?></li><? } ?></ul>`)
result, err := Generate(context.Background(), []string{templatePath, genericPath})
if err != nil {
t.Fatalf("Generate failed: %v (%v)", err, result.Diagnostics)
}
+69 -4
View File
@@ -2,7 +2,16 @@
package compiler
import "testing"
import (
"bytes"
"crypto/sha256"
"fmt"
"go/parser"
"go/token"
"reflect"
"strings"
"testing"
)
func FuzzCompileNeverPanics(f *testing.F) {
for _, seed := range []string{
@@ -14,13 +23,69 @@ func FuzzCompileNeverPanics(f *testing.F) {
"<?sando go\npackage p\nfunc F(v string)\n?>\n<script><!--<script></script>\n<?= v ?>\n<!--\n</script>\n-->",
"\xef\xbb\xbf\r\n<?sando go\r\npackage p\r\nfunc F()\r\n?>\r\n<p>x</p>",
} {
f.Add(seed)
f.Add(seed, "fuzz.sando")
}
f.Fuzz(func(t *testing.T, source string) {
_, _ = Compile("fuzz.sando", []byte(source))
f.Add("<?sando go\npackage p\nfunc F()\n?>\n<p>x</p>", "path%with\ncontrols\x00.sando")
f.Fuzz(func(t *testing.T, source, mapping string) {
if len(source) > 64<<10 || len(mapping) > 4<<10 {
t.Skip()
}
input := []byte(source)
before := append([]byte(nil), input...)
first, firstDiagnostics := compileWithMapping("fuzz.sando", input, mapping)
second, secondDiagnostics := compileWithMapping("fuzz.sando", input, mapping)
if !bytes.Equal(input, before) {
t.Fatal("compiler modified its source input")
}
if !reflect.DeepEqual(firstDiagnostics, secondDiagnostics) ||
first.SourcePath != second.SourcePath || first.OutputPath != second.OutputPath ||
first.Package != second.Package || first.Component != second.Component ||
first.Digest != second.Digest || !bytes.Equal(first.Code, second.Code) {
t.Fatal("repeated in-memory compilation was not deterministic")
}
for _, diagnostic := range firstDiagnostics {
if diagnostic.Path != "fuzz.sando" || diagnostic.Line < 1 || diagnostic.Column < 1 {
t.Fatalf("diagnostic has an invalid location: %#v", diagnostic)
}
if !validDiagnosticCode(diagnostic.Code) || strings.TrimSpace(diagnostic.Message) != diagnostic.Message || diagnostic.Message == "" {
t.Fatalf("diagnostic violates the public shape: %#v", diagnostic)
}
if diagnostic.Severity != SeverityError && diagnostic.Severity != SeverityWarning {
t.Fatalf("diagnostic has an invalid severity: %#v", diagnostic)
}
}
if len(first.Code) == 0 {
return
}
if first.SourcePath != "fuzz.sando" || first.OutputPath != "fuzz.sando.go" {
t.Fatalf("compiled paths are invalid: %#v", first)
}
if first.Digest != fmt.Sprintf("%x", sha256.Sum256(input)) {
t.Fatalf("source digest is not bound to the exact input: %s", first.Digest)
}
if _, err := parser.ParseFile(token.NewFileSet(), "fuzz.sando.go", first.Code, parser.AllErrors); err != nil {
t.Fatalf("successful compilation produced invalid Go: %v\n%s", err, first.Code)
}
for _, line := range bytes.Split(first.Code, []byte{'\n'}) {
if bytes.HasPrefix(line, []byte("//line ")) && (bytes.ContainsAny(line, "\r\x00") || bytes.Count(line, []byte(":")) < 2) {
t.Fatalf("source-map directive was not safely encoded: %q", line)
}
}
})
}
func validDiagnosticCode(code string) bool {
if len(code) != 7 || !strings.HasPrefix(code, "HIM") {
return false
}
for _, digit := range code[3:] {
if digit < '0' || digit > '9' {
return false
}
}
return true
}
func FuzzGoDelimiterNeverPanics(f *testing.F) {
for _, seed := range []string{`?>`, `"?>" ?>`, "`?>` ?>", `/* ?> */ ?>`, "// ?>\n?>", `'?' ?>`} {
f.Add(seed, uint8(0))
+467
View File
@@ -0,0 +1,467 @@
// SPDX-License-Identifier: AGPL-3.0-only
//go:build himesan_browser_evidence
package devserver
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync/atomic"
"testing"
"time"
"gamertan.com/sandwich-hime/internal/compiler"
)
const realBrowserTimeout = 20 * time.Second
func TestRealBrowserDevelopmentClient(t *testing.T) {
chrome := os.Getenv("HIMESAN_CHROME")
if chrome == "" {
t.Fatal("HIMESAN_CHROME must name the reviewed Chrome or Chromium executable")
}
if info, err := os.Stat(chrome); err != nil || info.IsDir() || info.Mode()&0o111 == 0 {
t.Fatalf("HIMESAN_CHROME is not an executable regular file: %q", chrome)
}
t.Run("diagnostic overlay and CSP", func(t *testing.T) {
harness := newRealBrowserHarness(t)
document := runRealBrowser(t, chrome, harness.url("/"), "browser evidence diagnostic", func() {
waitForRealBrowserSubscriber(t, harness.hub)
harness.hub.publish(Event{
Type: "diagnostic", Phase: "generate", Message: "browser evidence diagnostic",
Diagnostics: []Diagnostic{{Path: "views/home.sando", Line: 7, Column: 3, Code: "HIM1300", Message: "deliberate evidence fixture"}},
})
})
for _, want := range []string{"id=\"__himesan_overlay\"", "browser evidence diagnostic", "views/home.sando:7:3 [HIM1300]"} {
if !strings.Contains(document, want) {
t.Fatalf("browser DOM lacks %q:\n%s", want, document)
}
}
})
t.Run("reload", func(t *testing.T) {
harness := newRealBrowserHarness(t)
document := runRealBrowser(t, chrome, harness.url("/"), "version two", func() {
waitForRealBrowserSubscriber(t, harness.hub)
harness.version.Store(2)
harness.hub.publish(Event{Type: "reload", Phase: "serve", Message: "healthy application activated"})
})
if !strings.Contains(document, `<main id="page-version">version two</main>`) {
t.Fatalf("browser did not reload the selected document:\n%s", document)
}
})
t.Run("fragment and API exclusions", func(t *testing.T) {
harness := newRealBrowserHarness(t)
for _, path := range []string{"/fragment", "/api"} {
document := runRealBrowser(t, chrome, harness.url(path), "", nil)
if strings.Contains(document, "data-himesan-reload") || strings.Contains(document, "__himesan_overlay") {
t.Fatalf("development client leaked into %s:\n%s", path, document)
}
}
})
t.Run("generated document parsing", func(t *testing.T) {
root := writeGeneratedBrowserApplication(t)
cfg := DefaultConfig()
cfg.ProxyAddress = "127.0.0.1:0"
cfg.HealthPath = "/healthz"
supervisor, err := New(Options{
RootDir: root,
Config: cfg,
Generate: func(ctx context.Context) error {
_, generateErr := compiler.Generate(ctx, []string{root})
return generateErr
},
CacheDir: filepath.Join(t.TempDir(), "cache"),
PollInterval: 30 * time.Second,
Debounce: 25 * time.Millisecond,
BuildTimeout: 30 * time.Second,
StartupTimeout: integrationCandidateStartupTimeout,
ShutdownTimeout: 2 * time.Second,
})
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
runResult := make(chan error, 1)
go func() { runResult <- supervisor.Run(ctx) }()
proxyAddress := waitForProxyAddress(t, supervisor)
waitForBody(t, "http://"+proxyAddress+"/", "generated-browser-document")
document := runRealBrowser(t, chrome, "http://"+proxyAddress+"/", "generated-browser-document", nil)
for _, want := range []string{
`<main id="generated-browser-document"`,
`<h1 id="title">&lt;unsafe&gt; &amp; "quoted"</h1>`,
`<textarea id="note">&lt;/textarea&gt;&lt;script id="attacker"&gt;window.evidenceFailed=true&lt;/script&gt;</textarea>`,
`<a id="destination" href="/safe?q=a&amp;b=c">Open</a>`,
`<table id="table"><tbody><tr><td id="cell">&lt;unsafe&gt; &amp; "quoted"</td></tr></tbody></table>`,
} {
if !strings.Contains(document, want) {
t.Fatalf("generated browser DOM lacks %q:\n%s", want, document)
}
}
if strings.Contains(document, `<script id="attacker">`) {
t.Fatalf("hostile RCDATA became executable structure:\n%s", document)
}
cancel()
select {
case err := <-runResult:
if err != nil {
t.Fatalf("stop generated-document supervisor: %v", err)
}
case <-time.After(5 * time.Second):
t.Fatal("generated-document supervisor did not stop")
}
})
}
func writeGeneratedBrowserApplication(t *testing.T) string {
t.Helper()
root, err := filepath.EvalSymlinks(t.TempDir())
if err != nil {
t.Fatalf("resolve test-owned browser fixture: %v", err)
}
_, sourceFile, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("locate browser evidence source")
}
repositoryRoot := filepath.Clean(filepath.Join(filepath.Dir(sourceFile), "..", ".."))
files := map[string]string{
"go.mod": fmt.Sprintf("module example.test/himesan-browser-evidence\n\ngo 1.25\n\nrequire gamertan.com/sandwich-hime/sando v0.0.0\nreplace gamertan.com/sandwich-hime/sando => %s/sando\n", filepath.ToSlash(repositoryRoot)),
"page.sando": `<?sando go
package main
func Page(title string, note string, destination string)
?>
<!doctype html>
<html><head><title><?= title ?></title></head><body>
<main id="generated-browser-document" data-note="<?= note ?>">
<h1 id="title"><?= title ?></h1>
<textarea id="note"><?= note ?></textarea>
<a id="destination" href="<?= destination ?>">Open</a>
<table id="table"><tbody><tr><td id="cell"><?= title ?></td></tr></tbody></table>
</main>
</body></html>
`,
"main.go": `package main
import (
"net/http"
"os"
"gamertan.com/sandwich-hime/sando"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNoContent) })
mux.HandleFunc("/", func(w http.ResponseWriter, request *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := sando.Render(request.Context(), w, Page(
` + "`<unsafe> & \"quoted\"`" + `,
` + "`</textarea><script id=\"attacker\">window.evidenceFailed=true</script>`" + `,
"/safe?q=a&b=c",
)); err != nil {
panic(err)
}
})
if err := http.ListenAndServe(os.Getenv("HIMESAN_LISTEN_ADDR"), mux); err != nil {
panic(err)
}
}
`,
}
for name, contents := range files {
if err := os.WriteFile(filepath.Join(root, name), []byte(contents), 0o600); err != nil {
t.Fatal(err)
}
}
if _, err := compiler.Generate(context.Background(), []string{root}); err != nil {
t.Fatalf("generate browser evidence fixture: %v", err)
}
return root
}
type realBrowserHarness struct {
hub *eventHub
version atomic.Int32
upstream *httptest.Server
proxy *httptest.Server
}
func newRealBrowserHarness(t *testing.T) *realBrowserHarness {
t.Helper()
harness := &realBrowserHarness{hub: newEventHub()}
harness.version.Store(1)
harness.upstream = httptest.NewServer(http.HandlerFunc(harness.serveApplication))
development := newDevelopmentProxy(harness.hub)
upstreamURL, err := url.Parse(harness.upstream.URL)
if err != nil {
t.Fatal(err)
}
if err := development.setTarget(upstreamURL.Host); err != nil {
t.Fatal(err)
}
harness.proxy = httptest.NewServer(development)
proxyURL, err := url.Parse(harness.proxy.URL)
if err != nil {
t.Fatal(err)
}
if err := development.setAuthority(proxyURL.Host); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
harness.hub.close()
harness.proxy.Close()
harness.upstream.Close()
})
return harness
}
func (h *realBrowserHarness) url(path string) string {
return h.proxy.URL + path
}
func (h *realBrowserHarness) serveApplication(w http.ResponseWriter, request *http.Request) {
switch request.URL.Path {
case "/":
version := "one"
if h.version.Load() == 2 {
version = "two"
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Content-Security-Policy", "default-src 'none'; script-src 'none'; script-src-elem 'none'; connect-src 'none'")
fmt.Fprintf(w, `<!doctype html><html><body><main id="page-version">version %s</main></body></html>`, version)
case "/fragment":
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, `<p id="fragment">fragment only</p>`)
case "/api":
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"kind":"api","ok":true}`)
default:
http.NotFound(w, request)
}
}
func runRealBrowser(t *testing.T, chrome, target, want string, afterStart func()) string {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), realBrowserTimeout)
defer cancel()
toBrowserRead, toBrowserWrite, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
fromBrowserRead, fromBrowserWrite, err := os.Pipe()
if err != nil {
_ = toBrowserRead.Close()
_ = toBrowserWrite.Close()
t.Fatal(err)
}
defer toBrowserWrite.Close()
defer fromBrowserRead.Close()
stderrPath := filepath.Join(t.TempDir(), "chrome.stderr")
stderr, err := os.OpenFile(stderrPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
if err != nil {
t.Fatal(err)
}
devNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
if err != nil {
_ = stderr.Close()
t.Fatal(err)
}
command := exec.CommandContext(ctx, chrome,
"--headless=new",
"--disable-background-networking",
"--disable-component-update",
"--disable-default-apps",
"--disable-sync",
"--metrics-recording-only",
"--no-first-run",
"--no-default-browser-check",
"--user-data-dir="+t.TempDir(),
"--remote-debugging-pipe",
"about:blank",
)
command.ExtraFiles = []*os.File{toBrowserRead, fromBrowserWrite}
command.Stdout = devNull
command.Stderr = stderr
command.WaitDelay = 2 * time.Second
if err := command.Start(); err != nil {
_ = toBrowserRead.Close()
_ = fromBrowserWrite.Close()
_ = stderr.Close()
_ = devNull.Close()
t.Fatalf("start real browser: %v", err)
}
_ = toBrowserRead.Close()
_ = fromBrowserWrite.Close()
client := &devToolsPipe{reader: bufio.NewReader(fromBrowserRead), writer: toBrowserWrite}
targetResult := client.call(t, "", "Target.createTarget", map[string]any{"url": target})
var created struct {
TargetID string `json:"targetId"`
}
if err := json.Unmarshal(targetResult, &created); err != nil || created.TargetID == "" {
t.Fatalf("decode Chrome target: %v", err)
}
attachResult := client.call(t, "", "Target.attachToTarget", map[string]any{"targetId": created.TargetID, "flatten": true})
var attached struct {
SessionID string `json:"sessionId"`
}
if err := json.Unmarshal(attachResult, &attached); err != nil || attached.SessionID == "" {
t.Fatalf("decode Chrome session: %v", err)
}
client.call(t, attached.SessionID, "Runtime.enable", map[string]any{})
waitForBrowserDOM(t, client, attached.SessionID, "")
if afterStart != nil {
afterStart()
}
document := waitForBrowserDOM(t, client, attached.SessionID, want)
_ = client.callIgnoringClose("", "Browser.close", map[string]any{})
waitResult := make(chan error, 1)
go func() { waitResult <- command.Wait() }()
select {
case err := <-waitResult:
if err != nil && !errors.Is(err, exec.ErrWaitDelay) {
_ = stderr.Close()
contents, _ := os.ReadFile(stderrPath)
t.Fatalf("real browser failed: %v; stderr=%s", err, boundedBrowserOutput(string(contents)))
}
case <-time.After(5 * time.Second):
_ = command.Process.Kill()
t.Fatal("real browser did not exit after Browser.close")
}
_ = stderr.Close()
_ = devNull.Close()
return document
}
type devToolsPipe struct {
reader *bufio.Reader
writer io.Writer
nextID int
}
type devToolsEnvelope struct {
ID int `json:"id"`
Result json.RawMessage `json:"result"`
Error *struct {
Code int `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func (c *devToolsPipe) call(t *testing.T, session, method string, parameters map[string]any) json.RawMessage {
t.Helper()
c.nextID++
request := map[string]any{"id": c.nextID, "method": method, "params": parameters}
if session != "" {
request["sessionId"] = session
}
contents, err := json.Marshal(request)
if err != nil {
t.Fatal(err)
}
contents = append(contents, 0)
if _, err := c.writer.Write(contents); err != nil {
t.Fatalf("write Chrome DevTools request: %v", err)
}
for {
message, err := c.reader.ReadBytes(0)
if err != nil {
t.Fatalf("read Chrome DevTools response: %v", err)
}
if len(message) > 4<<20 {
t.Fatal("Chrome DevTools response exceeds 4 MiB")
}
var response devToolsEnvelope
if err := json.Unmarshal(bytes.TrimSuffix(message, []byte{0}), &response); err != nil {
t.Fatalf("decode Chrome DevTools response: %v", err)
}
if response.ID != c.nextID {
continue
}
if response.Error != nil {
t.Fatalf("Chrome DevTools %s failed (%d): %s", method, response.Error.Code, response.Error.Message)
}
return response.Result
}
}
func (c *devToolsPipe) callIgnoringClose(session, method string, parameters map[string]any) error {
c.nextID++
request := map[string]any{"id": c.nextID, "method": method, "params": parameters}
if session != "" {
request["sessionId"] = session
}
contents, err := json.Marshal(request)
if err != nil {
return err
}
_, err = c.writer.Write(append(contents, 0))
return err
}
func waitForBrowserDOM(t *testing.T, client *devToolsPipe, session, want string) string {
t.Helper()
deadline := time.Now().Add(10 * time.Second)
var last string
for time.Now().Before(deadline) {
result := client.call(t, session, "Runtime.evaluate", map[string]any{
"expression": "document.documentElement && document.documentElement.outerHTML",
"returnByValue": true,
})
var evaluated struct {
Result struct {
Type string `json:"type"`
Value string `json:"value"`
} `json:"result"`
}
if err := json.Unmarshal(result, &evaluated); err == nil && evaluated.Result.Type == "string" {
last = evaluated.Result.Value
if last != "" && (want == "" || strings.Contains(last, want)) {
return last
}
}
time.Sleep(20 * time.Millisecond)
}
t.Fatalf("browser DOM did not contain %q; last DOM:\n%s", want, last)
return ""
}
func waitForRealBrowserSubscriber(t *testing.T, hub *eventHub) {
t.Helper()
deadline := time.Now().Add(10 * time.Second)
for time.Now().Before(deadline) {
hub.mu.Lock()
count := len(hub.subscribers)
hub.mu.Unlock()
if count != 0 {
return
}
time.Sleep(10 * time.Millisecond)
}
t.Fatal("real browser did not establish the same-origin event stream")
}
func boundedBrowserOutput(value string) string {
const limit = 4096
if len(value) <= limit {
return value
}
return value[:limit] + "\n[Chrome output truncated]"
}
+77
View File
@@ -0,0 +1,77 @@
// SPDX-License-Identifier: AGPL-3.0-only
package devserver
import (
"encoding/json"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"testing"
)
func TestV1ConfigSchemaMatchesConfigTypeAndDefaults(t *testing.T) {
t.Parallel()
contents, err := os.ReadFile(filepath.Join("..", "..", "contracts", "himesan-config-v1.schema.json"))
if err != nil {
t.Fatal(err)
}
var schema struct {
AdditionalProperties bool `json:"additionalProperties"`
Required []string `json:"required"`
Properties map[string]json.RawMessage `json:"properties"`
}
if err := json.Unmarshal(contents, &schema); err != nil {
t.Fatal(err)
}
if schema.AdditionalProperties {
t.Fatal("v1 config schema must reject unknown fields")
}
if !reflect.DeepEqual(schema.Required, []string{"version"}) {
t.Fatalf("required config fields = %v, want [version]", schema.Required)
}
typeOfConfig := reflect.TypeOf(Config{})
fields := make([]string, 0, typeOfConfig.NumField())
for index := 0; index < typeOfConfig.NumField(); index++ {
name := strings.Split(typeOfConfig.Field(index).Tag.Get("json"), ",")[0]
fields = append(fields, name)
}
sort.Strings(fields)
properties := make([]string, 0, len(schema.Properties))
for name := range schema.Properties {
properties = append(properties, name)
}
sort.Strings(properties)
if !reflect.DeepEqual(fields, properties) {
t.Fatalf("Config JSON fields %v do not match schema properties %v", fields, properties)
}
defaults := DefaultConfig()
wantDefaults := map[string]any{
"version": float64(defaults.Version),
"sourceRoots": []any{"."},
"goPackage": defaults.GoPackage,
"appArgs": []any{},
"listenAddressEnv": defaults.ListenAddressEnv,
"healthPath": defaults.HealthPath,
"proxyAddress": defaults.ProxyAddress,
"additionalWatchRoots": []any{},
}
for name, want := range wantDefaults {
var property map[string]any
if err := json.Unmarshal(schema.Properties[name], &property); err != nil {
t.Fatal(err)
}
if name == "version" {
if !reflect.DeepEqual(property["const"], want) {
t.Fatalf("schema %s const = %#v, want %#v", name, property["const"], want)
}
continue
}
if !reflect.DeepEqual(property["default"], want) {
t.Fatalf("schema %s default = %#v, want %#v", name, property["default"], want)
}
}
}
+4 -1
View File
@@ -81,7 +81,7 @@ func (c *candidateProcess) cleanupProcessTree() error {
processTree := c.processTree
c.processTree = 0
c.mu.Unlock()
return cleanupProcess(c.command, processTree)
return acceptableStopError(cleanupProcess(c.command, processTree))
}
func (c *candidateProcess) stop(ctx context.Context) error {
@@ -119,6 +119,9 @@ func acceptableStopError(err error) error {
if err == nil {
return nil
}
if errors.Is(err, os.ErrProcessDone) {
return nil
}
var exitError *exec.ExitError
if errors.As(err, &exitError) {
return nil
+7
View File
@@ -87,6 +87,13 @@ func TestTaskkillArguments(t *testing.T) {
}
}
func TestAlreadyFinishedIsAnAcceptableStopResult(t *testing.T) {
t.Parallel()
if err := acceptableStopError(os.ErrProcessDone); err != nil {
t.Fatalf("already-finished process was treated as a stop failure: %v", err)
}
}
func TestManagedProcessHelper(t *testing.T) {
switch os.Getenv("HIMESAN_PROCESS_HELPER") {
case "":
+23 -9
View File
@@ -18,6 +18,8 @@ import (
"time"
)
const integrationCandidateStartupTimeout = 3 * time.Second
func TestSupervisorBuildsSwapsAndCleansUp(t *testing.T) {
if testing.Short() {
t.Skip("integration test builds temporary Go applications")
@@ -46,11 +48,14 @@ func TestSupervisorBuildsSwapsAndCleansUp(t *testing.T) {
t.Logf("supervisor event: type=%s phase=%s message=%s", event.Type, event.Phase, event.Message)
events <- event
},
CacheDir: filepath.Join(t.TempDir(), "cache"),
PollInterval: 25 * time.Millisecond,
Debounce: 25 * time.Millisecond,
BuildTimeout: 30 * time.Second,
StartupTimeout: 750 * time.Millisecond,
CacheDir: filepath.Join(t.TempDir(), "cache"),
PollInterval: 25 * time.Millisecond,
Debounce: 25 * time.Millisecond,
BuildTimeout: 30 * time.Second,
// Keep the production default unchanged. Native release runners may
// compile another Go line concurrently, so this integration test gives
// the temporary child enough time to be scheduled under honest load.
StartupTimeout: integrationCandidateStartupTimeout,
ShutdownTimeout: 2 * time.Second,
})
if err != nil {
@@ -128,7 +133,7 @@ func TestSupervisorClearsTargetWhenCurrentApplicationExits(t *testing.T) {
PollInterval: 30 * time.Second,
Debounce: 25 * time.Millisecond,
BuildTimeout: 30 * time.Second,
StartupTimeout: time.Second,
StartupTimeout: integrationCandidateStartupTimeout,
ShutdownTimeout: 2 * time.Second,
})
if err != nil {
@@ -140,7 +145,11 @@ func TestSupervisorClearsTargetWhenCurrentApplicationExits(t *testing.T) {
t.Cleanup(cancel)
proxyAddress := waitForProxyAddress(t, supervisor)
waitForBody(t, "http://"+proxyAddress+"/", "short lived")
// Observe the supervisor's ordered activation event instead of racing an
// HTTP request against the deliberately short-lived child. Other tests
// prove proxy serving; this test proves that an activated child which exits
// clears its selected target under scheduler and compiler load.
waitForEvent(t, events, "reload", "serve")
waitForPhase(t, events, "run")
if target := supervisor.proxy.target.Load(); target != nil {
t.Fatalf("proxy retained exited upstream %v", target)
@@ -290,17 +299,22 @@ func main() {
}
func waitForPhase(t *testing.T, events <-chan Event, phase string) {
t.Helper()
waitForEvent(t, events, "diagnostic", phase)
}
func waitForEvent(t *testing.T, events <-chan Event, eventType, phase string) {
t.Helper()
timer := time.NewTimer(10 * time.Second)
defer timer.Stop()
for {
select {
case event := <-events:
if event.Type == "diagnostic" && event.Phase == phase {
if event.Type == eventType && event.Phase == phase {
return
}
case <-timer.C:
t.Fatalf("did not receive %s diagnostic", phase)
t.Fatalf("did not receive %s/%s event", eventType, phase)
}
}
}
+5 -4
View File
@@ -15,6 +15,7 @@ import (
"time"
"gamertan.com/sandwich-hime/internal/compiler"
"gamertan.com/sandwich-hime/internal/testpath"
)
type protocolClient struct {
@@ -140,7 +141,7 @@ func (client *protocolClient) waitDiagnostics(t *testing.T, uri string, wantCode
}
func TestServerOverlayFeaturesAndNoWrites(t *testing.T) {
root := t.TempDir()
root := testpath.TempDir(t)
writeTestFile(t, filepath.Join(root, "go.mod"), "module example.test/project\n\ngo 1.25\n")
homePath := filepath.Join(root, "home.sando")
badgePath := filepath.Join(root, "cards", "badge.sando")
@@ -211,7 +212,7 @@ func TestServerOverlayFeaturesAndNoWrites(t *testing.T) {
}
func TestOverlayHonorsNestedModuleAndSymlinkBoundaries(t *testing.T) {
root := t.TempDir()
root := testpath.TempDir(t)
writeTestFile(t, filepath.Join(root, "go.mod"), "module example.test/root\n")
nestedPath := filepath.Join(root, "nested", "view.sando")
writeTestFile(t, filepath.Join(root, "nested", "go.mod"), "module example.test/nested\n")
@@ -240,7 +241,7 @@ func TestOverlayHonorsNestedModuleAndSymlinkBoundaries(t *testing.T) {
}
func TestServerRejectsMultipleRootsAndCanceledRequest(t *testing.T) {
root := t.TempDir()
root := testpath.TempDir(t)
server := &Server{initialized: true, snapshot: workspaceSnapshot{documents: map[string]document{}, analyses: map[string]compiler.DocumentAnalysis{}}}
ctx, cancel := context.WithCancel(context.Background())
cancel()
@@ -263,7 +264,7 @@ func TestServerRejectsMultipleRootsAndCanceledRequest(t *testing.T) {
}
func TestReindexCountsOpenOverlaysInWorkspaceLimit(t *testing.T) {
root := t.TempDir()
root := testpath.TempDir(t)
server := &Server{
root: root,
overlays: make(map[string]document, maxWorkspaceFiles+1),
+423
View File
@@ -0,0 +1,423 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Package releaseartifact creates deterministic Hime-san release archives and
// checksummed native-platform verification receipts.
package releaseartifact
import (
"archive/tar"
"compress/gzip"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"time"
)
const receiptSchema = "himesan.native-verification.v1"
var (
releaseVersionPattern = regexp.MustCompile(`^v(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$`)
gitObjectPattern = regexp.MustCompile(`^[0-9a-f]{40,64}$`)
digestPattern = regexp.MustCompile(`^[0-9a-f]{64}$`)
goVersionPattern = regexp.MustCompile(`^go(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)$`)
platformPattern = regexp.MustCompile(`^[a-z0-9]+$`)
)
// PackageOptions contains the reviewed inputs to one release archive.
type PackageOptions struct {
Version string
Commit string
Tree string
GoVersion string
GOOS string
GOARCH string
BinaryPath string
LicensePath string
ReleaseNotes string
OutputDirectory string
SourceDateEpoch int64
}
// PackageResult identifies the immutable unsigned release archive.
type PackageResult struct {
ArchivePath string `json:"archive_path"`
SHA256 string `json:"sha256"`
}
type buildInfo struct {
SchemaVersion int `json:"schema_version"`
Version string `json:"version"`
Commit string `json:"commit"`
Tree string `json:"tree"`
GoVersion string `json:"go_version"`
GOOS string `json:"goos"`
GOARCH string `json:"goarch"`
BinarySHA256 string `json:"binary_sha256"`
UnsignedBinarySHA256 string `json:"unsigned_binary_sha256,omitempty"`
UnsignedArchiveSHA256 string `json:"unsigned_archive_sha256,omitempty"`
SigningIdentifier string `json:"signing_identifier,omitempty"`
}
type spdxDocument struct {
SPDXVersion string `json:"spdxVersion"`
DataLicense string `json:"dataLicense"`
SPDXID string `json:"SPDXID"`
Name string `json:"name"`
DocumentNamespace string `json:"documentNamespace"`
CreationInfo creationInfo `json:"creationInfo"`
Packages []spdxPackage `json:"packages"`
}
type creationInfo struct {
Created string `json:"created"`
Creators []string `json:"creators"`
}
type spdxPackage struct {
Name string `json:"name"`
SPDXID string `json:"SPDXID"`
VersionInfo string `json:"versionInfo"`
DownloadLocation string `json:"downloadLocation"`
FilesAnalyzed bool `json:"filesAnalyzed"`
LicenseConcluded string `json:"licenseConcluded"`
LicenseDeclared string `json:"licenseDeclared"`
Checksums []spdxChecksum `json:"checksums"`
}
type spdxChecksum struct {
Algorithm string `json:"algorithm"`
ChecksumValue string `json:"checksumValue"`
}
// Receipt describes one completed native verification run.
type Receipt struct {
SchemaVersion string `json:"schema_version"`
Repository string `json:"repository"`
Commit string `json:"commit"`
Tree string `json:"tree"`
GOOS string `json:"goos"`
GOARCH string `json:"goarch"`
GoVersion string `json:"go_version"`
RunnerVersion string `json:"runner_version"`
RunnerName string `json:"runner_name"`
GeneratedDigest string `json:"generated_output_sha256"`
UnsignedArtifactSHA string `json:"unsigned_artifact_sha256,omitempty"`
SuccessfulGates []string `json:"successful_gates"`
CompletedAt string `json:"completed_at"`
}
type archiveMember struct {
name string
mode int64
data []byte
}
// Package creates a byte-reproducible unsigned tar.gz and its SHA-256 sidecar.
func Package(options PackageOptions) (PackageResult, error) {
if err := validatePackageOptions(options); err != nil {
return PackageResult{}, err
}
binary, err := os.ReadFile(options.BinaryPath)
if err != nil {
return PackageResult{}, fmt.Errorf("read binary: %w", err)
}
license, err := os.ReadFile(options.LicensePath)
if err != nil {
return PackageResult{}, fmt.Errorf("read license: %w", err)
}
releaseNotes, err := os.ReadFile(options.ReleaseNotes)
if err != nil {
return PackageResult{}, fmt.Errorf("read release notes: %w", err)
}
binaryDigest := digest(binary)
infoBytes, err := indentedJSON(buildInfo{
SchemaVersion: 1,
Version: options.Version,
Commit: options.Commit,
Tree: options.Tree,
GoVersion: options.GoVersion,
GOOS: options.GOOS,
GOARCH: options.GOARCH,
BinarySHA256: binaryDigest,
})
if err != nil {
return PackageResult{}, err
}
created := time.Unix(options.SourceDateEpoch, 0).UTC().Format(time.RFC3339)
sbomBytes, err := indentedJSON(spdxDocument{
SPDXVersion: "SPDX-2.3",
DataLicense: "CC0-1.0",
SPDXID: "SPDXRef-DOCUMENT",
Name: "himesan-" + options.Version + "-" + options.GOOS + "-" + options.GOARCH,
DocumentNamespace: "https://sandwichhime.com/spdx/himesan/" + options.Version + "/" + options.Commit + "/" + options.GOOS + "-" + options.GOARCH,
CreationInfo: creationInfo{
Created: created,
Creators: []string{"Organization: Gamertan"},
},
Packages: []spdxPackage{{
Name: "himesan",
SPDXID: "SPDXRef-Package-himesan",
VersionInfo: options.Version,
DownloadLocation: "NOASSERTION",
FilesAnalyzed: false,
LicenseConcluded: "AGPL-3.0-only",
LicenseDeclared: "AGPL-3.0-only",
Checksums: []spdxChecksum{{
Algorithm: "SHA256",
ChecksumValue: binaryDigest,
}},
}},
})
if err != nil {
return PackageResult{}, err
}
members := []archiveMember{
{name: "BUILDINFO.json", mode: 0o444, data: infoBytes},
{name: "LICENSE", mode: 0o444, data: license},
{name: "RELEASE.md", mode: 0o444, data: releaseNotes},
{name: "SBOM.spdx.json", mode: 0o444, data: sbomBytes},
{name: "himesan", mode: 0o555, data: binary},
}
checksumLines := make([]string, 0, len(members))
for _, member := range members {
checksumLines = append(checksumLines, digest(member.data)+" "+member.name)
}
members = append(members, archiveMember{
name: "SHA256SUMS", mode: 0o444,
data: []byte(strings.Join(checksumLines, "\n") + "\n"),
})
sort.Slice(members, func(i, j int) bool { return members[i].name < members[j].name })
if err := os.MkdirAll(options.OutputDirectory, 0o755); err != nil {
return PackageResult{}, fmt.Errorf("create output directory: %w", err)
}
base := "himesan-" + strings.TrimPrefix(options.Version, "v") + "-" + options.GOOS + "-" + options.GOARCH
archivePath := filepath.Join(options.OutputDirectory, base+".tar.gz")
if err := writeArchive(archivePath, base, members, time.Unix(options.SourceDateEpoch, 0).UTC()); err != nil {
return PackageResult{}, err
}
archive, err := os.ReadFile(archivePath)
if err != nil {
return PackageResult{}, fmt.Errorf("read completed archive: %w", err)
}
archiveDigest := digest(archive)
if err := writeExclusive(archivePath+".sha256", []byte(archiveDigest+" "+filepath.Base(archivePath)+"\n"), 0o444); err != nil {
return PackageResult{}, fmt.Errorf("write archive checksum: %w", err)
}
return PackageResult{ArchivePath: archivePath, SHA256: archiveDigest}, nil
}
// WriteReceipt validates, canonicalizes, and writes a receipt plus SHA sidecar.
func WriteReceipt(path string, receipt Receipt) (string, error) {
receipt.SchemaVersion = receiptSchema
if err := validateReceipt(receipt); err != nil {
return "", err
}
sort.Strings(receipt.SuccessfulGates)
contents, err := indentedJSON(receipt)
if err != nil {
return "", err
}
if err := writeExclusive(path, contents, 0o444); err != nil {
return "", fmt.Errorf("write receipt: %w", err)
}
checksum := digest(contents)
if err := writeExclusive(path+".sha256", []byte(checksum+" "+filepath.Base(path)+"\n"), 0o444); err != nil {
return "", fmt.Errorf("write receipt checksum: %w", err)
}
return checksum, nil
}
// DigestFiles returns a stable digest over sorted names and file contents.
func DigestFiles(paths []string) (string, error) {
if len(paths) == 0 {
return "", errors.New("at least one generated file is required")
}
sorted := append([]string(nil), paths...)
sort.Strings(sorted)
hash := sha256.New()
for _, path := range sorted {
contents, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("read %s: %w", path, err)
}
_, _ = io.WriteString(hash, filepath.ToSlash(path))
_, _ = hash.Write([]byte{0})
_, _ = hash.Write(contents)
_, _ = hash.Write([]byte{0})
}
return hex.EncodeToString(hash.Sum(nil)), nil
}
func validatePackageOptions(options PackageOptions) error {
for name, value := range map[string]string{
"version": options.Version, "commit": options.Commit, "tree": options.Tree,
"go version": options.GoVersion, "GOOS": options.GOOS, "GOARCH": options.GOARCH,
"binary": options.BinaryPath, "license": options.LicensePath,
"release notes": options.ReleaseNotes, "output directory": options.OutputDirectory,
} {
if strings.TrimSpace(value) == "" {
return fmt.Errorf("%s is required", name)
}
}
if options.SourceDateEpoch <= 0 {
return errors.New("source date epoch must be positive")
}
if !releaseVersionPattern.MatchString(options.Version) {
return errors.New("version must be a canonical v-prefixed semantic version without build metadata")
}
if !gitObjectPattern.MatchString(options.Commit) || !gitObjectPattern.MatchString(options.Tree) {
return errors.New("commit and tree must be full lowercase Git object identities")
}
if !goVersionPattern.MatchString(options.GoVersion) {
return errors.New("Go version must be a complete goX.Y.Z toolchain identity")
}
if !platformPattern.MatchString(options.GOOS) || !platformPattern.MatchString(options.GOARCH) {
return errors.New("GOOS and GOARCH must contain only lowercase letters and digits")
}
return nil
}
func validateReceipt(receipt Receipt) error {
for name, value := range map[string]string{
"repository": receipt.Repository, "commit": receipt.Commit, "tree": receipt.Tree,
"GOOS": receipt.GOOS, "GOARCH": receipt.GOARCH, "Go version": receipt.GoVersion,
"runner version": receipt.RunnerVersion, "runner name": receipt.RunnerName,
"generated digest": receipt.GeneratedDigest, "completed at": receipt.CompletedAt,
} {
if strings.TrimSpace(value) == "" {
return fmt.Errorf("%s is required", name)
}
}
if len(receipt.SuccessfulGates) == 0 {
return errors.New("at least one successful gate is required")
}
if !gitObjectPattern.MatchString(receipt.Commit) || !gitObjectPattern.MatchString(receipt.Tree) {
return errors.New("commit and tree must be full lowercase Git object identities")
}
if !goVersionPattern.MatchString(receipt.GoVersion) {
return errors.New("Go version must be a complete goX.Y.Z toolchain identity")
}
if !platformPattern.MatchString(receipt.GOOS) || !platformPattern.MatchString(receipt.GOARCH) {
return errors.New("GOOS and GOARCH must contain only lowercase letters and digits")
}
if !digestPattern.MatchString(receipt.GeneratedDigest) {
return errors.New("generated-output digest must be a lowercase SHA-256")
}
if receipt.UnsignedArtifactSHA != "" && !digestPattern.MatchString(receipt.UnsignedArtifactSHA) {
return errors.New("unsigned-artifact digest must be a lowercase SHA-256")
}
if _, err := time.Parse(time.RFC3339, receipt.CompletedAt); err != nil {
return fmt.Errorf("completed at must be RFC3339: %w", err)
}
return nil
}
func writeArchive(path, root string, members []archiveMember, modified time.Time) error {
file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
if err != nil {
return fmt.Errorf("create archive: %w", err)
}
success := false
defer func() {
_ = file.Close()
if !success {
_ = os.Remove(path)
}
}()
gzipWriter := gzip.NewWriter(file)
gzipWriter.Header.ModTime = modified
gzipWriter.Header.OS = 255
tarWriter := tar.NewWriter(gzipWriter)
for _, member := range members {
header := &tar.Header{
Name: root + "/" + member.name,
Mode: member.mode,
Size: int64(len(member.data)),
ModTime: modified,
AccessTime: time.Time{},
ChangeTime: time.Time{},
Uid: 0,
Gid: 0,
Uname: "",
Gname: "",
Format: tar.FormatUSTAR,
}
if err := tarWriter.WriteHeader(header); err != nil {
return fmt.Errorf("write archive header: %w", err)
}
if _, err := tarWriter.Write(member.data); err != nil {
return fmt.Errorf("write archive member: %w", err)
}
}
if err := tarWriter.Close(); err != nil {
return fmt.Errorf("close tar stream: %w", err)
}
if err := gzipWriter.Close(); err != nil {
return fmt.Errorf("close gzip stream: %w", err)
}
if err := file.Sync(); err != nil {
return fmt.Errorf("sync archive: %w", err)
}
if err := file.Close(); err != nil {
return fmt.Errorf("close archive: %w", err)
}
if err := os.Chmod(path, 0o444); err != nil {
return fmt.Errorf("set archive permissions: %w", err)
}
success = true
return nil
}
func writeExclusive(path string, contents []byte, mode os.FileMode) error {
file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, mode)
if err != nil {
return err
}
success := false
defer func() {
_ = file.Close()
if !success {
_ = os.Remove(path)
}
}()
if _, err := file.Write(contents); err != nil {
return err
}
if err := file.Sync(); err != nil {
return err
}
if err := file.Close(); err != nil {
return err
}
if err := os.Chmod(path, mode); err != nil {
return err
}
success = true
return nil
}
func indentedJSON(value any) ([]byte, error) {
contents, err := json.MarshalIndent(value, "", " ")
if err != nil {
return nil, fmt.Errorf("encode JSON: %w", err)
}
return append(contents, '\n'), nil
}
func digest(contents []byte) string {
sum := sha256.Sum256(contents)
return hex.EncodeToString(sum[:])
}
+103
View File
@@ -0,0 +1,103 @@
// SPDX-License-Identifier: AGPL-3.0-only
package releaseartifact
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func TestPackageIsByteReproducible(t *testing.T) {
directory := t.TempDir()
binary := writeFixture(t, directory, "himesan", "native binary")
license := writeFixture(t, directory, "LICENSE", "license")
releaseNotes := writeFixture(t, directory, "RELEASE.md", "release")
options := PackageOptions{
Version: "v1.0.0-rc.1", Commit: strings.Repeat("a", 40), Tree: strings.Repeat("b", 40), GoVersion: "go1.27.0",
GOOS: "darwin", GOARCH: "arm64", BinaryPath: binary, LicensePath: license,
ReleaseNotes: releaseNotes, SourceDateEpoch: 1_700_000_000,
}
options.OutputDirectory = filepath.Join(directory, "first")
first, err := Package(options)
if err != nil {
t.Fatal(err)
}
options.OutputDirectory = filepath.Join(directory, "second")
second, err := Package(options)
if err != nil {
t.Fatal(err)
}
if first.SHA256 != second.SHA256 {
t.Fatalf("repeated packages differ: %s != %s", first.SHA256, second.SHA256)
}
}
func TestWriteReceiptSortsGatesAndWritesChecksum(t *testing.T) {
path := filepath.Join(t.TempDir(), "receipt.json")
checksum, err := WriteReceipt(path, Receipt{
Repository: "example.test/development-source", Commit: strings.Repeat("a", 40), Tree: strings.Repeat("b", 40),
GOOS: "darwin", GOARCH: "arm64", GoVersion: "go1.27.0",
RunnerVersion: "v3.3.0", RunnerName: "mac", GeneratedDigest: strings.Repeat("c", 64),
SuccessfulGates: []string{"vet", "test"}, CompletedAt: time.Unix(1_700_000_000, 0).UTC().Format(time.RFC3339),
})
if err != nil {
t.Fatal(err)
}
if checksum == "" {
t.Fatal("empty checksum")
}
if _, err := os.Stat(path + ".sha256"); err != nil {
t.Fatal(err)
}
}
func TestPackageRejectsUnsafeVersionAndExistingArtifact(t *testing.T) {
directory := t.TempDir()
options := PackageOptions{
Version: "../../outside", Commit: strings.Repeat("a", 40), Tree: strings.Repeat("b", 40),
GoVersion: "go1.27.0", GOOS: "darwin", GOARCH: "arm64",
BinaryPath: writeFixture(t, directory, "himesan", "native binary"),
LicensePath: writeFixture(t, directory, "LICENSE", "license"),
ReleaseNotes: writeFixture(t, directory, "RELEASE.md", "release"),
OutputDirectory: filepath.Join(directory, "output"), SourceDateEpoch: 1_700_000_000,
}
if _, err := Package(options); err == nil {
t.Fatal("unsafe version was accepted")
}
options.Version = "v1.0.0-rc.1"
if _, err := Package(options); err != nil {
t.Fatal(err)
}
if _, err := Package(options); err == nil {
t.Fatal("existing immutable artifact was overwritten")
}
}
func TestDigestFilesBindsNamesAndBytes(t *testing.T) {
directory := t.TempDir()
one := writeFixture(t, directory, "one", "same")
two := writeFixture(t, directory, "two", "same")
forward, err := DigestFiles([]string{two, one})
if err != nil {
t.Fatal(err)
}
reverse, err := DigestFiles([]string{one, two})
if err != nil {
t.Fatal(err)
}
if forward != reverse {
t.Fatal("file ordering changed digest")
}
}
func writeFixture(t *testing.T, directory, name, contents string) string {
t.Helper()
path := filepath.Join(directory, name)
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
t.Fatal(err)
}
return path
}
+238
View File
@@ -0,0 +1,238 @@
// SPDX-License-Identifier: AGPL-3.0-only
package releaseartifact
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
"time"
"unicode/utf8"
)
const (
evidenceSchema = "himesan.release-evidence.v1"
evidenceManifestName = "RELEASE-EVIDENCE.json"
maximumEvidenceSize = 1 << 20
)
var requiredEvidenceFiles = []string{
"benchmark-methodology.md",
"development-supervisor.md",
"legal-review.md",
"native-platforms.md",
"security.md",
"signing-and-recovery.md",
"vanity-imports.md",
}
// EvidenceIdentity binds human-reviewed release evidence to one source state.
type EvidenceIdentity struct {
Repository string
Version string
Commit string
Tree string
ReviewedBy string
ReviewedAt string
}
type evidenceManifest struct {
SchemaVersion string `json:"schema_version"`
Repository string `json:"repository"`
Version string `json:"version"`
Commit string `json:"commit"`
Tree string `json:"tree"`
ReviewStatus string `json:"review_status"`
ReviewedBy string `json:"reviewed_by"`
ReviewedAt string `json:"reviewed_at"`
Files []evidenceFile `json:"files"`
}
type evidenceFile struct {
Path string `json:"path"`
SHA256 string `json:"sha256"`
}
// RequiredEvidenceFiles returns the fixed v1 human-review document set.
func RequiredEvidenceFiles() []string {
return append([]string(nil), requiredEvidenceFiles...)
}
// WriteEvidenceManifest seals the reviewed evidence documents without
// modifying them. The manifest is immutable and fails if it already exists.
func WriteEvidenceManifest(directory string, identity EvidenceIdentity) (string, error) {
if err := validateEvidenceIdentity(identity, true); err != nil {
return "", err
}
files, err := inspectEvidenceFiles(directory)
if err != nil {
return "", err
}
manifest := evidenceManifest{
SchemaVersion: evidenceSchema,
Repository: identity.Repository,
Version: identity.Version,
Commit: identity.Commit,
Tree: identity.Tree,
ReviewStatus: "reviewed",
ReviewedBy: identity.ReviewedBy,
ReviewedAt: identity.ReviewedAt,
Files: files,
}
contents, err := indentedJSON(manifest)
if err != nil {
return "", err
}
path := filepath.Join(directory, evidenceManifestName)
if err := writeExclusive(path, contents, 0o444); err != nil {
return "", fmt.Errorf("write release evidence manifest: %w", err)
}
return digest(contents), nil
}
// VerifyEvidenceManifest proves that the sealed evidence is for the expected
// source state and that every reviewed byte remains unchanged.
func VerifyEvidenceManifest(directory string, expected EvidenceIdentity) error {
if err := validateEvidenceIdentity(expected, false); err != nil {
return err
}
manifestPath := filepath.Join(directory, evidenceManifestName)
contents, err := readEvidenceFile(manifestPath, true)
if err != nil {
return fmt.Errorf("read release evidence manifest: %w", err)
}
decoder := json.NewDecoder(bytes.NewReader(contents))
decoder.DisallowUnknownFields()
var manifest evidenceManifest
if err := decoder.Decode(&manifest); err != nil {
return fmt.Errorf("decode release evidence manifest: %w", err)
}
if err := requireJSONEOF(decoder); err != nil {
return fmt.Errorf("decode release evidence manifest: %w", err)
}
if manifest.SchemaVersion != evidenceSchema {
return fmt.Errorf("release evidence schema = %q, want %q", manifest.SchemaVersion, evidenceSchema)
}
if manifest.ReviewStatus != "reviewed" {
return errors.New("release evidence review status must be reviewed")
}
actual := EvidenceIdentity{
Repository: manifest.Repository,
Version: manifest.Version,
Commit: manifest.Commit,
Tree: manifest.Tree,
ReviewedBy: manifest.ReviewedBy,
ReviewedAt: manifest.ReviewedAt,
}
if err := validateEvidenceIdentity(actual, true); err != nil {
return err
}
if actual.Repository != expected.Repository || actual.Version != expected.Version ||
actual.Commit != expected.Commit || actual.Tree != expected.Tree {
return errors.New("release evidence repository, version, commit, or tree does not match the candidate")
}
observed, err := inspectEvidenceFiles(directory)
if err != nil {
return err
}
if len(manifest.Files) != len(observed) {
return fmt.Errorf("release evidence manifest contains %d files, want %d", len(manifest.Files), len(observed))
}
for index := range observed {
if manifest.Files[index] != observed[index] {
return fmt.Errorf("release evidence file %q is missing, reordered, or has a changed digest", observed[index].Path)
}
}
return nil
}
func inspectEvidenceFiles(directory string) ([]evidenceFile, error) {
info, err := os.Lstat(directory)
if err != nil {
return nil, fmt.Errorf("inspect evidence directory: %w", err)
}
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
return nil, errors.New("evidence directory must be a real directory, not a symlink")
}
files := make([]evidenceFile, 0, len(requiredEvidenceFiles))
for _, name := range requiredEvidenceFiles {
contents, err := readEvidenceFile(filepath.Join(directory, name), false)
if err != nil {
return nil, fmt.Errorf("inspect %s: %w", name, err)
}
if !utf8.Valid(contents) || bytes.IndexByte(contents, 0) >= 0 {
return nil, fmt.Errorf("%s must be NUL-free UTF-8 text", name)
}
if !strings.Contains(string(contents), "# ") {
return nil, fmt.Errorf("%s must contain a Markdown heading", name)
}
files = append(files, evidenceFile{Path: name, SHA256: digest(contents)})
}
sort.Slice(files, func(i, j int) bool { return files[i].Path < files[j].Path })
return files, nil
}
func readEvidenceFile(path string, manifest bool) ([]byte, error) {
info, err := os.Lstat(path)
if err != nil {
return nil, err
}
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
return nil, errors.New("must be a regular file, not a symlink")
}
if info.Size() == 0 || info.Size() > maximumEvidenceSize {
return nil, fmt.Errorf("size must be between 1 and %d bytes", maximumEvidenceSize)
}
contents, err := os.ReadFile(path)
if err != nil {
return nil, err
}
if manifest && len(contents) > maximumEvidenceSize {
return nil, errors.New("manifest exceeds the size limit")
}
return contents, nil
}
func validateEvidenceIdentity(identity EvidenceIdentity, requireReview bool) error {
if strings.TrimSpace(identity.Repository) == "" || strings.ContainsAny(identity.Repository, "\r\n\x00") {
return errors.New("evidence repository is required and must be one line")
}
if !releaseVersionPattern.MatchString(identity.Version) {
return errors.New("evidence version must be a canonical v-prefixed semantic version")
}
if !gitObjectPattern.MatchString(identity.Commit) || !gitObjectPattern.MatchString(identity.Tree) {
return errors.New("evidence commit and tree must be full lowercase Git object identities")
}
if !requireReview {
return nil
}
if strings.TrimSpace(identity.ReviewedBy) == "" || strings.ContainsAny(identity.ReviewedBy, "\r\n\x00") {
return errors.New("evidence reviewer is required and must be one line")
}
reviewedAt, err := time.Parse(time.RFC3339, identity.ReviewedAt)
if err != nil {
return fmt.Errorf("evidence review time must be RFC3339: %w", err)
}
if reviewedAt.After(time.Now().UTC().Add(5 * time.Minute)) {
return errors.New("evidence review time cannot be in the future")
}
return nil
}
func requireJSONEOF(decoder *json.Decoder) error {
var extra any
if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
if err == nil {
return errors.New("multiple JSON values are not allowed")
}
return err
}
return nil
}
+123
View File
@@ -0,0 +1,123 @@
// SPDX-License-Identifier: AGPL-3.0-only
package releaseartifact
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestEvidenceManifestBindsReviewedContentAndIdentity(t *testing.T) {
directory := evidenceFixture(t)
identity := evidenceIdentity()
checksum, err := WriteEvidenceManifest(directory, identity)
if err != nil {
t.Fatal(err)
}
if !digestPattern.MatchString(checksum) {
t.Fatalf("manifest checksum = %q", checksum)
}
if err := VerifyEvidenceManifest(directory, identity); err != nil {
t.Fatal(err)
}
path := filepath.Join(directory, requiredEvidenceFiles[0])
if err := os.WriteFile(path, []byte("# Changed after review\n"), 0o600); err != nil {
t.Fatal(err)
}
if err := VerifyEvidenceManifest(directory, identity); err == nil {
t.Fatal("changed evidence bytes were accepted")
}
}
func TestEvidenceManifestRejectsCandidateMismatchAndUnknownFields(t *testing.T) {
t.Run("candidate identity", func(t *testing.T) {
directory := evidenceFixture(t)
identity := evidenceIdentity()
if _, err := WriteEvidenceManifest(directory, identity); err != nil {
t.Fatal(err)
}
identity.Commit = strings.Repeat("d", 40)
if err := VerifyEvidenceManifest(directory, identity); err == nil {
t.Fatal("mismatched candidate was accepted")
}
})
t.Run("unknown manifest field", func(t *testing.T) {
directory := evidenceFixture(t)
identity := evidenceIdentity()
if _, err := WriteEvidenceManifest(directory, identity); err != nil {
t.Fatal(err)
}
path := filepath.Join(directory, evidenceManifestName)
contents, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
contents = []byte(strings.Replace(string(contents), `"files":`, `"unexpected": true, "files":`, 1))
if err := os.Chmod(path, 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, contents, 0o600); err != nil {
t.Fatal(err)
}
if err := VerifyEvidenceManifest(directory, identity); err == nil {
t.Fatal("unknown manifest field was accepted")
}
})
}
func TestEvidenceManifestRejectsSymlinkAndPlaceholderDocuments(t *testing.T) {
t.Run("symlink", func(t *testing.T) {
directory := evidenceFixture(t)
name := requiredEvidenceFiles[0]
path := filepath.Join(directory, name)
target := filepath.Join(directory, "target.md")
if err := os.WriteFile(target, []byte("# Target\n"), 0o600); err != nil {
t.Fatal(err)
}
if err := os.Remove(path); err != nil {
t.Fatal(err)
}
if err := os.Symlink(target, path); err != nil {
t.Fatal(err)
}
if _, err := WriteEvidenceManifest(directory, evidenceIdentity()); err == nil {
t.Fatal("symlinked evidence was accepted")
}
})
t.Run("placeholder", func(t *testing.T) {
directory := evidenceFixture(t)
if err := os.WriteFile(filepath.Join(directory, requiredEvidenceFiles[0]), []byte("not reviewed\n"), 0o600); err != nil {
t.Fatal(err)
}
if _, err := WriteEvidenceManifest(directory, evidenceIdentity()); err == nil {
t.Fatal("heading-free placeholder was accepted")
}
})
}
func evidenceFixture(t *testing.T) string {
t.Helper()
directory := t.TempDir()
for _, name := range requiredEvidenceFiles {
if err := os.WriteFile(filepath.Join(directory, name), []byte("# Reviewed "+name+"\n\nExact bounded evidence.\n"), 0o600); err != nil {
t.Fatal(err)
}
}
return directory
}
func evidenceIdentity() EvidenceIdentity {
return EvidenceIdentity{
Repository: "gamertan/sandwich-hime",
Version: "v1.0.0-rc.1",
Commit: strings.Repeat("a", 40),
Tree: strings.Repeat("b", 40),
ReviewedBy: "release operator",
ReviewedAt: "2026-08-23T18:00:00Z",
}
}
+364
View File
@@ -0,0 +1,364 @@
// SPDX-License-Identifier: AGPL-3.0-only
package releaseartifact
import (
"archive/tar"
"bytes"
"compress/gzip"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
)
const maximumNativeArchiveSize = 256 << 20
var unsignedPackageMembers = map[string]os.FileMode{
"BUILDINFO.json": 0o444,
"LICENSE": 0o444,
"RELEASE.md": 0o444,
"SBOM.spdx.json": 0o444,
"SHA256SUMS": 0o444,
"himesan": 0o555,
}
// MacOSSigningOptions are the reviewed identities added after Developer ID
// signing changes the native binary bytes.
type MacOSSigningOptions struct {
Directory string
UnsignedArchiveSHA256 string
Identity string
Identifier string
FinalizedAt string
}
type signingInfo struct {
SchemaVersion int `json:"schema_version"`
UnsignedArchiveSHA256 string `json:"unsigned_archive_sha256"`
UnsignedBinarySHA256 string `json:"unsigned_binary_sha256"`
SignedBinarySHA256 string `json:"signed_binary_sha256"`
Identity string `json:"identity"`
Identifier string `json:"identifier"`
FinalizedAt string `json:"finalized_at"`
}
// ExtractVerifiedMacOSPackage verifies the approved unsigned archive and
// extracts its fixed file set without delegating path handling to system tar.
func ExtractVerifiedMacOSPackage(archivePath, expectedSHA256, outputDirectory string) (string, error) {
if !digestPattern.MatchString(expectedSHA256) {
return "", errors.New("approved archive digest must be a lowercase SHA-256")
}
info, err := os.Lstat(archivePath)
if err != nil {
return "", fmt.Errorf("inspect unsigned archive: %w", err)
}
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
return "", errors.New("unsigned archive must be a regular file, not a symlink")
}
if info.Size() <= 0 || info.Size() > maximumNativeArchiveSize {
return "", errors.New("unsigned archive size is outside the permitted range")
}
archive, err := os.ReadFile(archivePath)
if err != nil {
return "", fmt.Errorf("read unsigned archive: %w", err)
}
if digest(archive) != expectedSHA256 {
return "", errors.New("unsigned archive does not match the approved digest")
}
members, root, err := readUnsignedPackage(archive)
if err != nil {
return "", err
}
if err := validateUnsignedPackage(members, root); err != nil {
return "", err
}
outputInfo, err := os.Lstat(outputDirectory)
if err != nil {
return "", fmt.Errorf("inspect extraction directory: %w", err)
}
if !outputInfo.IsDir() || outputInfo.Mode()&os.ModeSymlink != 0 {
return "", errors.New("extraction destination must be a real directory")
}
rootPath := filepath.Join(outputDirectory, root)
if err := os.Mkdir(rootPath, 0o700); err != nil {
return "", fmt.Errorf("create extracted package root: %w", err)
}
success := false
defer func() {
if !success {
_ = os.RemoveAll(rootPath)
}
}()
for name, mode := range unsignedPackageMembers {
if err := writeExclusive(filepath.Join(rootPath, name), members[name], mode); err != nil {
return "", fmt.Errorf("extract %s: %w", name, err)
}
}
success = true
return rootPath, nil
}
// FinalizeSignedMacOSDistribution replaces unsigned binary provenance with a
// complete signed-distribution record and regenerates every internal checksum.
func FinalizeSignedMacOSDistribution(options MacOSSigningOptions) error {
if !digestPattern.MatchString(options.UnsignedArchiveSHA256) {
return errors.New("unsigned archive digest must be a lowercase SHA-256")
}
if strings.TrimSpace(options.Identity) == "" || strings.ContainsAny(options.Identity, "\r\n\x00") {
return errors.New("signing identity is required and must be one line")
}
if strings.TrimSpace(options.Identifier) == "" || strings.ContainsAny(options.Identifier, "\r\n\x00") {
return errors.New("signing identifier is required and must be one line")
}
if _, err := time.Parse(time.RFC3339, options.FinalizedAt); err != nil {
return fmt.Errorf("finalization time must be RFC3339: %w", err)
}
for name := range unsignedPackageMembers {
if _, err := readRegularFile(filepath.Join(options.Directory, name), maximumNativeArchiveSize); err != nil {
return fmt.Errorf("inspect signed distribution %s: %w", name, err)
}
}
buildPath := filepath.Join(options.Directory, "BUILDINFO.json")
buildContents, err := os.ReadFile(buildPath)
if err != nil {
return fmt.Errorf("read unsigned build information: %w", err)
}
var build buildInfo
if err := decodeStrictJSON(buildContents, &build); err != nil {
return fmt.Errorf("decode unsigned build information: %w", err)
}
if build.SchemaVersion != 1 || !digestPattern.MatchString(build.BinarySHA256) || build.GOOS != "darwin" || build.GOARCH != "arm64" {
return errors.New("unsigned build information is not a supported Darwin/arm64 package")
}
unsignedBinaryDigest := build.BinarySHA256
signedBinary, err := os.ReadFile(filepath.Join(options.Directory, "himesan"))
if err != nil {
return fmt.Errorf("read signed binary: %w", err)
}
signedBinaryDigest := digest(signedBinary)
if signedBinaryDigest == unsignedBinaryDigest {
return errors.New("Developer ID signing did not change the native binary bytes")
}
build.SchemaVersion = 2
build.BinarySHA256 = signedBinaryDigest
build.UnsignedBinarySHA256 = unsignedBinaryDigest
build.UnsignedArchiveSHA256 = options.UnsignedArchiveSHA256
build.SigningIdentifier = options.Identifier
newBuild, err := indentedJSON(build)
if err != nil {
return err
}
sbomPath := filepath.Join(options.Directory, "SBOM.spdx.json")
sbomContents, err := os.ReadFile(sbomPath)
if err != nil {
return fmt.Errorf("read unsigned SBOM: %w", err)
}
var sbom spdxDocument
if err := decodeStrictJSON(sbomContents, &sbom); err != nil {
return fmt.Errorf("decode unsigned SBOM: %w", err)
}
if len(sbom.Packages) != 1 || len(sbom.Packages[0].Checksums) != 1 ||
sbom.Packages[0].Checksums[0].Algorithm != "SHA256" ||
sbom.Packages[0].Checksums[0].ChecksumValue != unsignedBinaryDigest {
return errors.New("unsigned SBOM does not match the unsigned binary")
}
sbom.Packages[0].Checksums[0].ChecksumValue = signedBinaryDigest
newSBOM, err := indentedJSON(sbom)
if err != nil {
return err
}
signingContents, err := indentedJSON(signingInfo{
SchemaVersion: 1,
UnsignedArchiveSHA256: options.UnsignedArchiveSHA256,
UnsignedBinarySHA256: unsignedBinaryDigest,
SignedBinarySHA256: signedBinaryDigest,
Identity: options.Identity,
Identifier: options.Identifier,
FinalizedAt: options.FinalizedAt,
})
if err != nil {
return err
}
if err := replaceRegularFile(buildPath, newBuild, 0o444); err != nil {
return fmt.Errorf("replace signed build information: %w", err)
}
if err := replaceRegularFile(sbomPath, newSBOM, 0o444); err != nil {
return fmt.Errorf("replace signed SBOM: %w", err)
}
if err := writeExclusive(filepath.Join(options.Directory, "SIGNING.json"), signingContents, 0o444); err != nil {
return fmt.Errorf("write signing information: %w", err)
}
checksumNames := []string{"BUILDINFO.json", "LICENSE", "RELEASE.md", "SBOM.spdx.json", "SIGNING.json", "himesan"}
var checksumLines []string
for _, name := range checksumNames {
contents, err := os.ReadFile(filepath.Join(options.Directory, name))
if err != nil {
return fmt.Errorf("read signed distribution member %s: %w", name, err)
}
checksumLines = append(checksumLines, digest(contents)+" "+name)
}
checksums := []byte(strings.Join(checksumLines, "\n") + "\n")
if err := replaceRegularFile(filepath.Join(options.Directory, "SHA256SUMS"), checksums, 0o444); err != nil {
return fmt.Errorf("replace signed distribution checksums: %w", err)
}
return nil
}
func readUnsignedPackage(archive []byte) (map[string][]byte, string, error) {
gzipReader, err := gzip.NewReader(bytes.NewReader(archive))
if err != nil {
return nil, "", fmt.Errorf("open unsigned gzip archive: %w", err)
}
defer gzipReader.Close()
tarReader := tar.NewReader(io.LimitReader(gzipReader, maximumNativeArchiveSize+1))
members := make(map[string][]byte, len(unsignedPackageMembers))
root := ""
var totalSize int64
for {
header, err := tarReader.Next()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return nil, "", fmt.Errorf("read unsigned tar archive: %w", err)
}
if header.Typeflag != tar.TypeReg || header.Size < 0 || header.Size > maximumNativeArchiveSize {
return nil, "", errors.New("unsigned archive contains a non-regular or oversized member")
}
totalSize += header.Size
if totalSize > maximumNativeArchiveSize {
return nil, "", errors.New("unsigned archive expands beyond the permitted size")
}
clean := filepath.ToSlash(filepath.Clean(header.Name))
parts := strings.Split(clean, "/")
if len(parts) != 2 || parts[0] == "" || parts[0] == "." || parts[0] == ".." {
return nil, "", fmt.Errorf("unsigned archive member has an unsafe path: %q", header.Name)
}
if root == "" {
root = parts[0]
} else if root != parts[0] {
return nil, "", errors.New("unsigned archive contains more than one package root")
}
name := parts[1]
expectedMode, ok := unsignedPackageMembers[name]
if !ok || os.FileMode(header.Mode).Perm() != expectedMode || header.Linkname != "" {
return nil, "", fmt.Errorf("unsigned archive member %q has an unexpected name, mode, or link", name)
}
if _, exists := members[name]; exists {
return nil, "", fmt.Errorf("unsigned archive repeats member %q", name)
}
contents, err := io.ReadAll(io.LimitReader(tarReader, header.Size+1))
if err != nil || int64(len(contents)) != header.Size {
return nil, "", fmt.Errorf("read unsigned archive member %q", name)
}
members[name] = contents
}
if err := gzipReader.Close(); err != nil {
return nil, "", fmt.Errorf("finish unsigned gzip archive: %w", err)
}
return members, root, nil
}
func validateUnsignedPackage(members map[string][]byte, root string) error {
if len(members) != len(unsignedPackageMembers) || root == "" {
return errors.New("unsigned archive does not contain the exact release file set")
}
var build buildInfo
if err := decodeStrictJSON(members["BUILDINFO.json"], &build); err != nil {
return fmt.Errorf("decode unsigned build information: %w", err)
}
if build.SchemaVersion != 1 || build.GOOS != "darwin" || build.GOARCH != "arm64" ||
!releaseVersionPattern.MatchString(build.Version) || root != "himesan-"+strings.TrimPrefix(build.Version, "v")+"-darwin-arm64" ||
!gitObjectPattern.MatchString(build.Commit) || !gitObjectPattern.MatchString(build.Tree) ||
!goVersionPattern.MatchString(build.GoVersion) || build.BinarySHA256 != digest(members["himesan"]) {
return errors.New("unsigned build information does not match the archive")
}
var sbom spdxDocument
if err := decodeStrictJSON(members["SBOM.spdx.json"], &sbom); err != nil {
return fmt.Errorf("decode unsigned SBOM: %w", err)
}
if len(sbom.Packages) != 1 || len(sbom.Packages[0].Checksums) != 1 ||
sbom.Packages[0].Checksums[0].Algorithm != "SHA256" ||
sbom.Packages[0].Checksums[0].ChecksumValue != build.BinarySHA256 {
return errors.New("unsigned SBOM does not match the native binary")
}
expectedNames := []string{"BUILDINFO.json", "LICENSE", "RELEASE.md", "SBOM.spdx.json", "himesan"}
var expectedLines []string
for _, name := range expectedNames {
expectedLines = append(expectedLines, digest(members[name])+" "+name)
}
if string(members["SHA256SUMS"]) != strings.Join(expectedLines, "\n")+"\n" {
return errors.New("unsigned package checksum manifest does not match its members")
}
return nil
}
func readRegularFile(path string, maximum int64) ([]byte, error) {
info, err := os.Lstat(path)
if err != nil {
return nil, err
}
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 || info.Size() < 0 || info.Size() > maximum {
return nil, errors.New("must be a bounded regular file, not a symlink")
}
return os.ReadFile(path)
}
func decodeStrictJSON(contents []byte, target any) error {
decoder := json.NewDecoder(bytes.NewReader(contents))
decoder.DisallowUnknownFields()
if err := decoder.Decode(target); err != nil {
return err
}
return requireJSONEOF(decoder)
}
func replaceRegularFile(path string, contents []byte, mode os.FileMode) error {
info, err := os.Lstat(path)
if err != nil {
return err
}
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
return errors.New("replacement target must be a regular file, not a symlink")
}
temporary, err := os.CreateTemp(filepath.Dir(path), ".himesan-release-*")
if err != nil {
return err
}
temporaryPath := temporary.Name()
success := false
defer func() {
_ = temporary.Close()
if !success {
_ = os.Remove(temporaryPath)
}
}()
if _, err := temporary.Write(contents); err != nil {
return err
}
if err := temporary.Sync(); err != nil {
return err
}
if err := temporary.Close(); err != nil {
return err
}
if err := os.Chmod(temporaryPath, mode); err != nil {
return err
}
if err := os.Rename(temporaryPath, path); err != nil {
return err
}
success = true
return nil
}
+181
View File
@@ -0,0 +1,181 @@
// SPDX-License-Identifier: AGPL-3.0-only
package releaseartifact
import (
"archive/tar"
"bytes"
"compress/gzip"
"os"
"path/filepath"
"strings"
"testing"
)
func TestExtractAndFinalizeSignedMacOSDistribution(t *testing.T) {
archive, archiveDigest := macOSPackageFixture(t)
extraction := t.TempDir()
root, err := ExtractVerifiedMacOSPackage(archive, archiveDigest, extraction)
if err != nil {
t.Fatal(err)
}
binaryPath := filepath.Join(root, "himesan")
unsignedBinary, err := os.ReadFile(binaryPath)
if err != nil {
t.Fatal(err)
}
if err := os.Chmod(binaryPath, 0o700); err != nil {
t.Fatal(err)
}
binary, err := os.OpenFile(binaryPath, os.O_APPEND|os.O_WRONLY, 0)
if err != nil {
t.Fatal(err)
}
if _, err := binary.WriteString("developer-id-signature"); err != nil {
t.Fatal(err)
}
if err := binary.Close(); err != nil {
t.Fatal(err)
}
options := MacOSSigningOptions{
Directory: root, UnsignedArchiveSHA256: archiveDigest,
Identity: "Developer ID Application: Example (TEAMID)",
Identifier: "com.gamertan.sandwich-hime.himesan",
FinalizedAt: "2026-08-23T18:00:00Z",
}
if err := FinalizeSignedMacOSDistribution(options); err != nil {
t.Fatal(err)
}
var build buildInfo
buildContents, err := os.ReadFile(filepath.Join(root, "BUILDINFO.json"))
if err != nil {
t.Fatal(err)
}
if err := decodeStrictJSON(buildContents, &build); err != nil {
t.Fatal(err)
}
signedBinary, err := os.ReadFile(binaryPath)
if err != nil {
t.Fatal(err)
}
if build.SchemaVersion != 2 || build.BinarySHA256 != digest(signedBinary) ||
build.UnsignedBinarySHA256 != digest(unsignedBinary) || build.UnsignedArchiveSHA256 != archiveDigest ||
build.SigningIdentifier != options.Identifier {
t.Fatalf("signed build information is incomplete: %#v", build)
}
var signing signingInfo
signingContents, err := os.ReadFile(filepath.Join(root, "SIGNING.json"))
if err != nil {
t.Fatal(err)
}
if err := decodeStrictJSON(signingContents, &signing); err != nil {
t.Fatal(err)
}
if signing.SignedBinarySHA256 != build.BinarySHA256 || signing.UnsignedBinarySHA256 != build.UnsignedBinarySHA256 {
t.Fatalf("signing record does not match build information: %#v", signing)
}
var sbom spdxDocument
sbomContents, err := os.ReadFile(filepath.Join(root, "SBOM.spdx.json"))
if err != nil {
t.Fatal(err)
}
if err := decodeStrictJSON(sbomContents, &sbom); err != nil {
t.Fatal(err)
}
if got := sbom.Packages[0].Checksums[0].ChecksumValue; got != build.BinarySHA256 {
t.Fatalf("signed SBOM checksum = %s, want %s", got, build.BinarySHA256)
}
assertChecksumManifest(t, root, []string{"BUILDINFO.json", "LICENSE", "RELEASE.md", "SBOM.spdx.json", "SIGNING.json", "himesan"})
if err := FinalizeSignedMacOSDistribution(options); err == nil {
t.Fatal("signed distribution was finalized twice")
}
}
func TestExtractVerifiedMacOSPackageRejectsSubstitutionAndSymlink(t *testing.T) {
archive, archiveDigest := macOSPackageFixture(t)
if _, err := ExtractVerifiedMacOSPackage(archive, strings.Repeat("0", 64), t.TempDir()); err == nil {
t.Fatal("archive substitution was accepted")
}
symlink := filepath.Join(t.TempDir(), "candidate.tar.gz")
if err := os.Symlink(archive, symlink); err != nil {
t.Fatal(err)
}
if _, err := ExtractVerifiedMacOSPackage(symlink, archiveDigest, t.TempDir()); err == nil {
t.Fatal("symlinked archive was accepted")
}
unsafeArchive := filepath.Join(t.TempDir(), "unsafe.tar.gz")
destination := t.TempDir()
escapeName := filepath.Base(destination) + "-outside"
unsafeContents := unsafeTarGzip(t, "../"+escapeName, []byte("not a package"))
if err := os.WriteFile(unsafeArchive, unsafeContents, 0o600); err != nil {
t.Fatal(err)
}
if _, err := ExtractVerifiedMacOSPackage(unsafeArchive, digest(unsafeContents), destination); err == nil {
t.Fatal("archive path traversal was accepted")
}
if _, err := os.Stat(filepath.Join(filepath.Dir(destination), escapeName)); !os.IsNotExist(err) {
t.Fatal("unsafe archive wrote outside the extraction directory")
}
}
func macOSPackageFixture(t *testing.T) (string, string) {
t.Helper()
directory := t.TempDir()
options := PackageOptions{
Version: "v1.0.0-rc.1", Commit: strings.Repeat("a", 40), Tree: strings.Repeat("b", 40),
GoVersion: "go1.27.0", GOOS: "darwin", GOARCH: "arm64",
BinaryPath: writeFixture(t, directory, "himesan", "unsigned Mach-O fixture"),
LicensePath: writeFixture(t, directory, "LICENSE", "licence"),
ReleaseNotes: writeFixture(t, directory, "RELEASE.md", "release"),
OutputDirectory: filepath.Join(directory, "package"), SourceDateEpoch: 1_700_000_000,
}
result, err := Package(options)
if err != nil {
t.Fatal(err)
}
return result.ArchivePath, result.SHA256
}
func assertChecksumManifest(t *testing.T, directory string, names []string) {
t.Helper()
var lines []string
for _, name := range names {
contents, err := os.ReadFile(filepath.Join(directory, name))
if err != nil {
t.Fatal(err)
}
lines = append(lines, digest(contents)+" "+name)
}
checksums, err := os.ReadFile(filepath.Join(directory, "SHA256SUMS"))
if err != nil {
t.Fatal(err)
}
if got, want := string(checksums), strings.Join(lines, "\n")+"\n"; got != want {
t.Fatalf("checksum manifest mismatch\ngot:\n%s\nwant:\n%s", got, want)
}
}
func unsafeTarGzip(t *testing.T, name string, contents []byte) []byte {
t.Helper()
var output bytes.Buffer
gzipWriter := gzip.NewWriter(&output)
tarWriter := tar.NewWriter(gzipWriter)
if err := tarWriter.WriteHeader(&tar.Header{Name: name, Mode: 0o444, Size: int64(len(contents)), Typeflag: tar.TypeReg}); err != nil {
t.Fatal(err)
}
if _, err := tarWriter.Write(contents); err != nil {
t.Fatal(err)
}
if err := tarWriter.Close(); err != nil {
t.Fatal(err)
}
if err := gzipWriter.Close(); err != nil {
t.Fatal(err)
}
return output.Bytes()
}
+220
View File
@@ -0,0 +1,220 @@
// SPDX-License-Identifier: AGPL-3.0-only
package releaseartifact
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"time"
)
const nativeReceiptMaximumAge = 30 * 24 * time.Hour
var runnerVersionPattern = regexp.MustCompile(`^v(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)$`)
type nativeLane struct {
directory string
goos string
goarch string
goVersion string
runner string
}
var requiredNativeLanes = []nativeLane{
{directory: "darwin-arm64-go1.26.7", goos: "darwin", goarch: "arm64", goVersion: "go1.26.7", runner: "macbook-air-himesan-darwin-arm64"},
{directory: "darwin-arm64-go1.27.0", goos: "darwin", goarch: "arm64", goVersion: "go1.27.0", runner: "macbook-air-himesan-darwin-arm64"},
{directory: "linux-amd64-go1.26.7", goos: "linux", goarch: "amd64", goVersion: "go1.26.7", runner: "cliff-himesan-linux-amd64"},
{directory: "linux-amd64-go1.27.0", goos: "linux", goarch: "amd64", goVersion: "go1.27.0", runner: "cliff-himesan-linux-amd64"},
}
var requiredNativeGates = []string{
"build", "consumer", "contracts", "fuzz", "generation", "package",
"public-snapshot", "race", "test", "vet", "vulnerability",
}
// NativeReceiptExpectation identifies the exact source verified by all native
// runner lanes.
type NativeReceiptExpectation struct {
Repository string
Commit string
Tree string
}
// NativeReceiptSummary is safe to include in human release evidence.
type NativeReceiptSummary struct {
SchemaVersion string `json:"schema_version"`
Repository string `json:"repository"`
Commit string `json:"commit"`
Tree string `json:"tree"`
GeneratedDigest string `json:"generated_output_sha256"`
Lanes []NativeLaneSummary `json:"lanes"`
}
// NativeLaneSummary records one verified native receipt without local paths.
type NativeLaneSummary struct {
Platform string `json:"platform"`
GoVersion string `json:"go_version"`
RunnerVersion string `json:"runner_version"`
ArtifactSHA256 string `json:"unsigned_artifact_sha256"`
ReceiptSHA256 string `json:"receipt_sha256"`
CompletedAt string `json:"completed_at"`
}
// VerifyNativeReceiptSet requires all four maintained target/toolchain lanes,
// validates their checksum sidecars, and proves identical generated output.
func VerifyNativeReceiptSet(directory string, expected NativeReceiptExpectation) (NativeReceiptSummary, error) {
if strings.TrimSpace(expected.Repository) == "" || strings.ContainsAny(expected.Repository, "\r\n\x00") {
return NativeReceiptSummary{}, errors.New("native receipt repository is required and must be one line")
}
if !gitObjectPattern.MatchString(expected.Commit) || !gitObjectPattern.MatchString(expected.Tree) {
return NativeReceiptSummary{}, errors.New("native receipt commit and tree must be full lowercase Git object identities")
}
if err := validateNativeDirectory(directory); err != nil {
return NativeReceiptSummary{}, err
}
summary := NativeReceiptSummary{
SchemaVersion: "himesan.native-receipt-set.v1",
Repository: expected.Repository, Commit: expected.Commit, Tree: expected.Tree,
}
for _, lane := range requiredNativeLanes {
receipt, receiptDigest, err := readNativeLane(filepath.Join(directory, lane.directory))
if err != nil {
return NativeReceiptSummary{}, fmt.Errorf("verify native lane %s: %w", lane.directory, err)
}
if receipt.SchemaVersion != receiptSchema || receipt.Repository != expected.Repository ||
receipt.Commit != expected.Commit || receipt.Tree != expected.Tree ||
receipt.GOOS != lane.goos || receipt.GOARCH != lane.goarch ||
receipt.GoVersion != lane.goVersion || receipt.RunnerName != lane.runner {
return NativeReceiptSummary{}, fmt.Errorf("native lane %s does not match its source, platform, toolchain, or runner", lane.directory)
}
if !runnerVersionPattern.MatchString(receipt.RunnerVersion) || !digestPattern.MatchString(receipt.UnsignedArtifactSHA) {
return NativeReceiptSummary{}, fmt.Errorf("native lane %s has an invalid runner or artifact identity", lane.directory)
}
completedAt, err := time.Parse(time.RFC3339, receipt.CompletedAt)
if err != nil || completedAt.After(time.Now().UTC().Add(5*time.Minute)) || time.Since(completedAt) > nativeReceiptMaximumAge {
return NativeReceiptSummary{}, fmt.Errorf("native lane %s is not fresh, completed, RFC3339 evidence", lane.directory)
}
if err := requireNativeGates(receipt.SuccessfulGates); err != nil {
return NativeReceiptSummary{}, fmt.Errorf("native lane %s: %w", lane.directory, err)
}
if summary.GeneratedDigest == "" {
summary.GeneratedDigest = receipt.GeneratedDigest
} else if summary.GeneratedDigest != receipt.GeneratedDigest {
return NativeReceiptSummary{}, errors.New("native lanes did not produce identical generated output")
}
summary.Lanes = append(summary.Lanes, NativeLaneSummary{
Platform: lane.goos + "/" + lane.goarch, GoVersion: lane.goVersion,
RunnerVersion: receipt.RunnerVersion, ArtifactSHA256: receipt.UnsignedArtifactSHA,
ReceiptSHA256: receiptDigest, CompletedAt: receipt.CompletedAt,
})
}
return summary, nil
}
func validateNativeDirectory(directory string) error {
info, err := os.Lstat(directory)
if err != nil {
return fmt.Errorf("inspect native evidence directory: %w", err)
}
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
return errors.New("native evidence must be a real directory")
}
entries, err := os.ReadDir(directory)
if err != nil {
return fmt.Errorf("read native evidence directory: %w", err)
}
expected := make([]string, 0, len(requiredNativeLanes))
for _, lane := range requiredNativeLanes {
expected = append(expected, lane.directory)
}
observed := make([]string, 0, len(entries))
for _, entry := range entries {
observed = append(observed, entry.Name())
}
sort.Strings(expected)
sort.Strings(observed)
if !equalStrings(expected, observed) {
return fmt.Errorf("native evidence directories = %v, want exactly %v", observed, expected)
}
return nil
}
func readNativeLane(directory string) (Receipt, string, error) {
info, err := os.Lstat(directory)
if err != nil {
return Receipt{}, "", err
}
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
return Receipt{}, "", errors.New("lane must be a real directory")
}
entries, err := os.ReadDir(directory)
if err != nil {
return Receipt{}, "", err
}
if len(entries) != 2 || entries[0].Name() != "TEND-CI-VERIFICATION.json" || entries[1].Name() != "TEND-CI-VERIFICATION.json.sha256" {
return Receipt{}, "", errors.New("lane must contain only its receipt and checksum sidecar")
}
receiptPath := filepath.Join(directory, "TEND-CI-VERIFICATION.json")
contents, err := readRegularFile(receiptPath, maximumEvidenceSize)
if err != nil || len(contents) == 0 {
return Receipt{}, "", errors.New("receipt must be a non-empty bounded regular file")
}
receiptDigest := digest(contents)
sidecar, err := readRegularFile(receiptPath+".sha256", 512)
if err != nil {
return Receipt{}, "", fmt.Errorf("read receipt checksum: %w", err)
}
expectedSidecar := receiptDigest + " TEND-CI-VERIFICATION.json\n"
if string(sidecar) != expectedSidecar {
return Receipt{}, "", errors.New("receipt checksum sidecar does not match")
}
decoder := json.NewDecoder(bytes.NewReader(contents))
decoder.DisallowUnknownFields()
var receipt Receipt
if err := decoder.Decode(&receipt); err != nil {
return Receipt{}, "", fmt.Errorf("decode receipt: %w", err)
}
if err := requireJSONEOF(decoder); err != nil {
return Receipt{}, "", fmt.Errorf("decode receipt: %w", err)
}
if err := validateReceipt(receipt); err != nil {
return Receipt{}, "", err
}
return receipt, receiptDigest, nil
}
func requireNativeGates(gates []string) error {
observed := make(map[string]bool, len(gates))
for _, gate := range gates {
if observed[gate] {
return fmt.Errorf("successful gate %q is duplicated", gate)
}
observed[gate] = true
}
for _, gate := range requiredNativeGates {
if !observed[gate] {
return fmt.Errorf("required successful gate %q is missing", gate)
}
}
return nil
}
func equalStrings(left, right []string) bool {
if len(left) != len(right) {
return false
}
for index := range left {
if left[index] != right[index] {
return false
}
}
return true
}
+109
View File
@@ -0,0 +1,109 @@
// SPDX-License-Identifier: AGPL-3.0-only
package releaseartifact
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func TestVerifyNativeReceiptSetRequiresEveryLaneAndMatchingGeneratedOutput(t *testing.T) {
expected := NativeReceiptExpectation{
Repository: "gamertan/sandwich-hime",
Commit: strings.Repeat("a", 40), Tree: strings.Repeat("b", 40),
}
directory := nativeReceiptFixture(t, expected, strings.Repeat("c", 64))
summary, err := VerifyNativeReceiptSet(directory, expected)
if err != nil {
t.Fatal(err)
}
if summary.GeneratedDigest != strings.Repeat("c", 64) || len(summary.Lanes) != 4 {
t.Fatalf("native summary is incomplete: %#v", summary)
}
t.Run("source mismatch", func(t *testing.T) {
mismatch := expected
mismatch.Commit = strings.Repeat("d", 40)
if _, err := VerifyNativeReceiptSet(directory, mismatch); err == nil {
t.Fatal("mismatched source commit was accepted")
}
})
t.Run("generated mismatch", func(t *testing.T) {
mismatched := nativeReceiptFixture(t, expected, strings.Repeat("c", 64))
lane := requiredNativeLanes[0]
laneDirectory := filepath.Join(mismatched, lane.directory)
if err := os.RemoveAll(laneDirectory); err != nil {
t.Fatal(err)
}
writeNativeLaneFixture(t, laneDirectory, lane, expected, strings.Repeat("e", 64))
if _, err := VerifyNativeReceiptSet(mismatched, expected); err == nil {
t.Fatal("different generated output was accepted")
}
})
}
func TestVerifyNativeReceiptSetRejectsTamperingAndUnexpectedFiles(t *testing.T) {
expected := NativeReceiptExpectation{
Repository: "gamertan/sandwich-hime",
Commit: strings.Repeat("a", 40), Tree: strings.Repeat("b", 40),
}
t.Run("checksum", func(t *testing.T) {
directory := nativeReceiptFixture(t, expected, strings.Repeat("c", 64))
sidecar := filepath.Join(directory, requiredNativeLanes[0].directory, "TEND-CI-VERIFICATION.json.sha256")
if err := os.Chmod(sidecar, 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(sidecar, []byte(strings.Repeat("0", 64)+" TEND-CI-VERIFICATION.json\n"), 0o600); err != nil {
t.Fatal(err)
}
if _, err := VerifyNativeReceiptSet(directory, expected); err == nil {
t.Fatal("tampered checksum was accepted")
}
})
t.Run("unexpected entry", func(t *testing.T) {
directory := nativeReceiptFixture(t, expected, strings.Repeat("c", 64))
if err := os.WriteFile(filepath.Join(directory, "notes.txt"), []byte("not a receipt"), 0o600); err != nil {
t.Fatal(err)
}
if _, err := VerifyNativeReceiptSet(directory, expected); err == nil {
t.Fatal("unexpected native evidence entry was accepted")
}
})
}
func nativeReceiptFixture(t *testing.T, expected NativeReceiptExpectation, generatedDigest string) string {
t.Helper()
directory := t.TempDir()
for _, lane := range requiredNativeLanes {
writeNativeLaneFixture(t, filepath.Join(directory, lane.directory), lane, expected, generatedDigest)
}
return directory
}
func writeNativeLaneFixture(t *testing.T, directory string, lane nativeLane, expected NativeReceiptExpectation, generatedDigest string) {
t.Helper()
if err := os.Mkdir(directory, 0o700); err != nil {
t.Fatal(err)
}
runnerVersion := "v3.1.0"
if lane.goos == "darwin" {
runnerVersion = "v3.3.0"
}
receipt := Receipt{
Repository: expected.Repository, Commit: expected.Commit, Tree: expected.Tree,
GOOS: lane.goos, GOARCH: lane.goarch, GoVersion: lane.goVersion,
RunnerVersion: runnerVersion, RunnerName: lane.runner,
GeneratedDigest: generatedDigest, UnsignedArtifactSHA: strings.Repeat("f", 64),
SuccessfulGates: append([]string(nil), requiredNativeGates...),
CompletedAt: time.Now().UTC().Add(-time.Minute).Format(time.RFC3339),
}
if _, err := WriteReceipt(filepath.Join(directory, "TEND-CI-VERIFICATION.json"), receipt); err != nil {
t.Fatal(err)
}
}
+36
View File
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Package testpath provides filesystem helpers for tests that exercise
// Hime-san's deliberate symlink boundaries.
package testpath
import (
"os"
"path/filepath"
"testing"
)
// TempDir returns the physical path to a fresh directory owned by the test.
//
// macOS commonly exposes its temporary directory through /var even though
// /var is a root-owned system symlink to /private/var. Resolving a directory
// immediately after testing.TB creates it keeps tests portable without
// teaching production path validation to follow user-controlled symlinks.
func TempDir(t testing.TB) string {
t.Helper()
directory := t.TempDir()
resolved, err := filepath.EvalSymlinks(directory)
if err != nil {
t.Fatalf("resolve test temporary directory: %v", err)
}
resolved = filepath.Clean(resolved)
info, err := os.Lstat(resolved)
if err != nil {
t.Fatalf("inspect resolved test temporary directory: %v", err)
}
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
t.Fatalf("resolved test temporary directory is not a physical directory: %s", resolved)
}
return resolved
}
+19
View File
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: AGPL-3.0-only
package testpath
import (
"path/filepath"
"testing"
)
func TestTempDirReturnsPhysicalPath(t *testing.T) {
directory := TempDir(t)
resolved, err := filepath.EvalSymlinks(directory)
if err != nil {
t.Fatal(err)
}
if filepath.Clean(resolved) != directory {
t.Fatalf("TempDir() = %q, physical path = %q", directory, resolved)
}
}