Files
bsdaily/internal/bsdaily/verify.go
T
clawbot c16f177575
check / check (push) Successful in 5m14s
Adopt the shared .golangci.yml and fix the code to it (closes #6)
Vendor .golangci.yml byte-identical from sneak/prompts at cc440118 and
move the Dockerfile lint phase to golangci-lint v2.14.0 by the digest
REPO_POLICIES.md names. Fix the code to that config with flags, help
text, output files, SQL and the order of steps unchanged; long
functions are split into named steps.

Judgement call: the extraction transaction is now rolled back on every
early return; the old deferred rollback missed most failures and could
dereference a nil transaction.
Thirteen //nolint directives (gosec, unconvert, mnd, unqueryvet), each
with its reason.

Model: opus-5-5
2026-10-06 14:24:48 +02:00

148 lines
3.4 KiB
Go

package bsdaily
import (
"context"
"errors"
"fmt"
"log/slog"
"os"
"os/exec"
"strings"
)
var (
errEmptyDecompressed = errors.New("decompressed content is empty")
errNotSQL = errors.New("decompressed content does not look like SQL")
)
// killCat terminates the zstdcat process, ignoring the benign case where it
// has already exited (e.g. after receiving SIGPIPE when head closed the pipe)
// and logging any other failure.
func killCat(cmd *exec.Cmd) {
if cmd.Process == nil {
return
}
err := cmd.Process.Kill()
if err != nil && !errors.Is(err, os.ErrProcessDone) {
slog.Warn("failed to kill zstdcat process", "error", err)
}
}
// VerifyOutput checks that the compressed file at path passes zstdmt's
// integrity test and that its first lines look like SQL.
func VerifyOutput(path string) error {
ctx := context.Background()
slog.Info("running zstdmt integrity check")
//nolint:gosec // path is an output file this package named
testCmd := exec.CommandContext(ctx, "zstdmt", "--test", path)
var testStderr strings.Builder
testCmd.Stderr = &testStderr
err := testCmd.Run()
if err != nil {
return fmt.Errorf("zstdmt --test failed: %w; stderr: %s",
err, testStderr.String())
}
slog.Info("zstdmt integrity check passed")
slog.Info("verifying SQL content")
content, err := readDecompressedHead(ctx, path)
if err != nil {
return err
}
err = checkLooksLikeSQL(content)
if err != nil {
return err
}
slog.Info("SQL content verification passed")
return nil
}
// readDecompressedHead returns the first verificationHeadLines lines of
// the decompressed file at path, read through `zstdcat path | head`.
func readDecompressedHead(ctx context.Context, path string) (string, error) {
//nolint:gosec // path is an output file this package named
catCmd := exec.CommandContext(ctx, "zstdcat", path)
//nolint:gosec // the argument is built from a constant
headCmd := exec.CommandContext(ctx, "head",
fmt.Sprintf("-%d", verificationHeadLines))
pipe, err := catCmd.StdoutPipe()
if err != nil {
return "", fmt.Errorf("creating zstdcat pipe: %w", err)
}
headCmd.Stdin = pipe
var headOut strings.Builder
headCmd.Stdout = &headOut
err = catCmd.Start()
if err != nil {
return "", fmt.Errorf("starting zstdcat: %w", err)
}
err = headCmd.Start()
if err != nil {
killCat(catCmd) // Clean up if head fails to start
return "", fmt.Errorf("starting head: %w", err)
}
// Wait for head first (it will exit when it has enough lines)
err = headCmd.Wait()
if err != nil {
killCat(catCmd)
return "", fmt.Errorf("head command failed: %w", err)
}
// Kill zstdcat since head closed the pipe (expected SIGPIPE)
killCat(catCmd)
_ = catCmd.Wait() // Reap the process
return headOut.String(), nil
}
// checkLooksLikeSQL returns an error when content is empty or contains
// none of the keywords expected near the start of a `sqlite3 .dump`.
func checkLooksLikeSQL(content string) error {
if len(content) == 0 {
return errEmptyDecompressed
}
hasSQLMarker := false
for _, marker := range []string{
"BEGIN TRANSACTION", "CREATE TABLE", "INSERT INTO", "PRAGMA",
} {
if strings.Contains(content, marker) {
hasSQLMarker = true
break
}
}
const verificationSampleBytes = 200
if !hasSQLMarker {
return fmt.Errorf("%w; first %d bytes: %s", errNotSQL,
verificationSampleBytes,
content[:min(verificationSampleBytes, len(content))])
}
return nil
}