//go:build windows package backup import ( "context" "errors" "os" "path/filepath" "testing" "github.com/aritmolab/thothii/tools/tht/internal/safeio" ) func TestStageArchiveProtectsWindowsStagingArtifactsWithOwnerOnlyACLs(t *testing.T) { installation := preflightTestInstallation(t) if err := os.MkdirAll(installation.ControlDirectory(), 0o700); err != nil { t.Fatal(err) } archive := filepath.Join(t.TempDir(), "valid.zip") writePreflightArchive(t, archive, preflightArchiveSpec{ entries: []preflightArchiveEntry{{path: "configuration/operator.env", body: []byte("safe")}}, }) result, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, permissivePreflightDependencies()) if err != nil { t.Fatal(err) } defer result.CloseArchive() staged, err := result.StageArchive(context.Background()) if err != nil { t.Fatal(err) } defer staged.Close() if err := staged.parent.Validate(); err != nil { t.Fatalf("retained staging root ACL = %v, want owner-only", err) } if err := staged.file.Close(); err != nil { t.Fatal(err) } staged.file = nil contents, found, err := staged.parent.ReadRegular(staged.name, 1<<20) if err != nil || !found || len(contents) == 0 { t.Fatalf("retained staged archive read = found:%t bytes:%d error:%v, want owner-only regular", found, len(contents), err) } } func TestStageArchiveRetainsTwoFilesThroughOneWindowsRootCapability(t *testing.T) { installation := preflightTestInstallation(t) archives := []string{filepath.Join(t.TempDir(), "candidate.zip"), filepath.Join(t.TempDir(), "recovery.zip")} for _, archive := range archives { writePreflightArchive(t, archive, preflightArchiveSpec{ entries: []preflightArchiveEntry{{path: "configuration/operator.env", body: []byte("safe")}}, }) } candidate, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archives[0], Confirm: true}, permissivePreflightDependencies()) if err != nil { t.Fatal(err) } defer candidate.CloseArchive() recovery, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archives[1], Confirm: true}, permissivePreflightDependencies()) if err != nil { t.Fatal(err) } defer recovery.CloseArchive() candidateStage, err := candidate.StageArchive(context.Background()) if err != nil { t.Fatal(err) } recoveryStage, err := recovery.stageArchiveAlongside(context.Background(), candidateStage) if err != nil { _ = candidateStage.Close() t.Fatal(err) } if candidateStage.parent != recoveryStage.parent { t.Fatal("paired stages did not share the retained root capability") } if err := candidateStage.Close(); err != nil { _ = recoveryStage.Close() t.Fatal(err) } if err := recoveryStage.file.Close(); err != nil { _ = recoveryStage.Close() t.Fatal(err) } recoveryStage.file = nil if _, found, err := recoveryStage.parent.ReadRegular(recoveryStage.name, 1<<20); err != nil || !found { _ = recoveryStage.Close() t.Fatalf("recovery stage after candidate cleanup = found:%t err:%v", found, err) } if err := recoveryStage.Close(); err != nil { t.Fatal(err) } } func TestStageArchiveCloseUsesPinnedRootAfterAncestorSwap(t *testing.T) { installation := preflightTestInstallation(t) if err := os.MkdirAll(installation.ControlDirectory(), 0o700); err != nil { t.Fatal(err) } archive := filepath.Join(t.TempDir(), "valid.zip") writePreflightArchive(t, archive, preflightArchiveSpec{ entries: []preflightArchiveEntry{{path: "configuration/operator.env", body: []byte("safe")}}, }) result, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, permissivePreflightDependencies()) if err != nil { t.Fatal(err) } defer result.CloseArchive() outsideParent, err := filepath.EvalSymlinks(t.TempDir()) if err != nil { t.Fatal(err) } outside := filepath.Join(outsideParent, "outside") if err := os.Mkdir(outside, 0o700); err != nil { t.Fatal(err) } if err := safeio.ProtectPrivateDirectory(outside); err != nil { t.Fatal(err) } sentinelPath := filepath.Join(outside, "sentinel") sentinel := []byte("outside sentinel") if err := os.WriteFile(sentinelPath, sentinel, 0o600); err != nil { t.Fatal(err) } attemptedSwap := false restoreHook := safeio.SetPrivateDirectoryTestHookForTest(func(stage string) { if stage != "before-stage-archive-remove" || attemptedSwap { return } attemptedSwap = true if err := os.Rename(result.stagingRoot, result.stagingRoot+"-moved"); err == nil { t.Fatal("staging-root rename succeeded while Close retained its directory handle") } }) defer restoreHook() staged, err := result.StageArchive(context.Background()) if err != nil { t.Fatal(err) } stagedName := staged.name if filepath.Dir(staged.path) != result.stagingRoot { t.Fatalf("staged archive directory = %q, want %q", filepath.Dir(staged.path), result.stagingRoot) } if err := staged.Close(); err != nil { t.Fatal(err) } if !attemptedSwap { t.Fatal("stage archive cleanup hook did not run") } if _, err := os.Stat(filepath.Join(result.stagingRoot, stagedName)); !errors.Is(err, os.ErrNotExist) { t.Fatalf("staging archive = %v, want os.ErrNotExist", err) } gotSentinel, err := os.ReadFile(sentinelPath) if err != nil { t.Fatal(err) } if string(gotSentinel) != string(sentinel) { t.Fatalf("outside sentinel = %q, want %q", gotSentinel, sentinel) } }