check / check (push) Successful in 5m14s
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
148 lines
3.4 KiB
Go
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
|
|
}
|