Adopt the shared .golangci.yml and fix the code to it (closes #6)
check / check (push) Successful in 5m14s
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
This commit was merged in pull request #12.
This commit is contained in:
+81
-19
@@ -1,6 +1,7 @@
|
||||
package bsdaily
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
@@ -9,6 +10,11 @@ import (
|
||||
"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.
|
||||
@@ -16,70 +22,126 @@ func killCat(cmd *exec.Cmd) {
|
||||
if cmd.Process == nil {
|
||||
return
|
||||
}
|
||||
if err := cmd.Process.Kill(); err != nil && !errors.Is(err, os.ErrProcessDone) {
|
||||
|
||||
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")
|
||||
testCmd := exec.Command("zstdmt", "--test", path)
|
||||
|
||||
//nolint:gosec // path is an output file this package named
|
||||
testCmd := exec.CommandContext(ctx, "zstdmt", "--test", path)
|
||||
|
||||
var testStderr strings.Builder
|
||||
|
||||
testCmd.Stderr = &testStderr
|
||||
if err := testCmd.Run(); err != nil {
|
||||
return fmt.Errorf("zstdmt --test failed: %w; stderr: %s", err, testStderr.String())
|
||||
|
||||
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")
|
||||
catCmd := exec.Command("zstdcat", path)
|
||||
headCmd := exec.Command("head", fmt.Sprintf("-%d", verificationHeadLines))
|
||||
|
||||
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)
|
||||
return "", fmt.Errorf("creating zstdcat pipe: %w", err)
|
||||
}
|
||||
|
||||
headCmd.Stdin = pipe
|
||||
|
||||
var headOut strings.Builder
|
||||
|
||||
headCmd.Stdout = &headOut
|
||||
|
||||
if err := catCmd.Start(); err != nil {
|
||||
return fmt.Errorf("starting zstdcat: %w", err)
|
||||
err = catCmd.Start()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("starting zstdcat: %w", err)
|
||||
}
|
||||
if err := headCmd.Start(); err != nil {
|
||||
|
||||
err = headCmd.Start()
|
||||
if err != nil {
|
||||
killCat(catCmd) // Clean up if head fails to start
|
||||
return fmt.Errorf("starting head: %w", err)
|
||||
|
||||
return "", fmt.Errorf("starting head: %w", err)
|
||||
}
|
||||
|
||||
// Wait for head first (it will exit when it has enough lines)
|
||||
if err := headCmd.Wait(); err != nil {
|
||||
err = headCmd.Wait()
|
||||
if err != nil {
|
||||
killCat(catCmd)
|
||||
return fmt.Errorf("head command failed: %w", err)
|
||||
|
||||
return "", fmt.Errorf("head command failed: %w", err)
|
||||
}
|
||||
|
||||
// Kill zstdcat since head closed the pipe (expected SIGPIPE)
|
||||
killCat(catCmd)
|
||||
|
||||
_ = catCmd.Wait() // Reap the process
|
||||
|
||||
content := headOut.String()
|
||||
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 fmt.Errorf("decompressed content is empty")
|
||||
return errEmptyDecompressed
|
||||
}
|
||||
|
||||
hasSQLMarker := false
|
||||
for _, marker := range []string{"BEGIN TRANSACTION", "CREATE TABLE", "INSERT INTO", "PRAGMA"} {
|
||||
|
||||
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("decompressed content does not look like SQL; first %d bytes: %s",
|
||||
verificationSampleBytes, content[:min(verificationSampleBytes, len(content))])
|
||||
return fmt.Errorf("%w; first %d bytes: %s", errNotSQL,
|
||||
verificationSampleBytes,
|
||||
content[:min(verificationSampleBytes, len(content))])
|
||||
}
|
||||
|
||||
slog.Info("SQL content verification passed")
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user