fix(agent): surface par2/install/NFS failures instead of degrading silently
- usenet: Par2Verify/Repair return ErrPar2NotInstalled (was nil="verified"); pipeline surfaces it via Result.VerifyNote + WARNING — a download that shipped parity but couldn't be checked is delivered UNVERIFIED, not verified. - funnel: pin cloudflared version + verify a baked-in SHA-256 (was `latest` + ELF-magic only) — a malicious/broken upstream release isn't pulled silently. - stream: makeReadable verifies the file actually opens after chmod and warns clearly (NFS root_squash / SMB uid mapping) instead of a cryptic later EPERM. - WireGuard endpoint pin dropped from the debt list (reseller uses direct config, no pin).
This commit is contained in:
parent
27bee8cdf4
commit
3d51013935
9 changed files with 319 additions and 43 deletions
|
|
@ -1,24 +1,36 @@
|
|||
package postprocess
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Par2Available checks if par2cmdline is installed.
|
||||
func Par2Available() bool {
|
||||
// ErrPar2NotInstalled is returned by Par2Verify/Par2Repair when parity data is
|
||||
// present but the `par2` binary is missing. The caller MUST surface this rather
|
||||
// than treat it as "verified OK" — a download that shipped parity but could not
|
||||
// be checked is delivered UNVERIFIED, not verified.
|
||||
var ErrPar2NotInstalled = errors.New("par2 not installed")
|
||||
|
||||
// par2Lookup probes whether the par2 binary is on PATH. It's a package var so
|
||||
// tests can simulate a missing binary without touching the real PATH.
|
||||
var par2Lookup = func() bool {
|
||||
_, err := exec.LookPath("par2")
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Par2Verify verifies files using a par2 file.
|
||||
// Returns nil if verification passes, error otherwise.
|
||||
// Par2Available checks if par2cmdline is installed.
|
||||
func Par2Available() bool { return par2Lookup() }
|
||||
|
||||
// Par2Verify verifies files using a par2 file. Returns nil on success,
|
||||
// ErrPar2NotInstalled when the binary is missing (parity present but unchecked —
|
||||
// the caller must surface it, NOT treat it as verified), a *Par2RepairableError
|
||||
// when repair is possible, or another error on failure.
|
||||
func Par2Verify(par2File string) error {
|
||||
if !Par2Available() {
|
||||
log.Printf("[usenet] par2 not installed, skipping verification")
|
||||
return nil
|
||||
return ErrPar2NotInstalled
|
||||
}
|
||||
|
||||
cmd := exec.Command("par2", "verify", par2File)
|
||||
|
|
@ -42,7 +54,7 @@ func Par2Verify(par2File string) error {
|
|||
// Par2Repair attempts to repair files using par2 parity data.
|
||||
func Par2Repair(par2File string) error {
|
||||
if !Par2Available() {
|
||||
return fmt.Errorf("par2 not installed")
|
||||
return ErrPar2NotInstalled
|
||||
}
|
||||
|
||||
cmd := exec.Command("par2", "repair", par2File)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package postprocess
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
|
@ -16,6 +17,12 @@ type Result struct {
|
|||
Files []string // all final files
|
||||
Repaired bool // whether par2 repair was needed
|
||||
Extracted bool // whether archive extraction was performed
|
||||
// VerifyNote is non-empty when par2 verification was DEGRADED — parity shipped
|
||||
// but could not be confirmed (par2 missing, repair failed, verify error). The
|
||||
// download is still delivered, but the caller surfaces this so the user knows
|
||||
// the file is unverified rather than silently assuming it's good. Empty means
|
||||
// either "verified OK" or "no parity shipped" — both are non-degraded.
|
||||
VerifyNote string
|
||||
}
|
||||
|
||||
// Options configures post-processing behavior.
|
||||
|
|
@ -29,21 +36,37 @@ type Options struct {
|
|||
func Process(dir string, downloadedFiles map[string]string, opts Options) (*Result, error) {
|
||||
result := &Result{}
|
||||
|
||||
// Step 1: Par2 verification and repair
|
||||
// Step 1: Par2 verification and repair. Parity is optional, so a missing
|
||||
// binary or a failed repair does NOT abort the download — but it MUST be
|
||||
// surfaced (result.VerifyNote + a WARNING) instead of silently delivering an
|
||||
// unverified file as if it had passed.
|
||||
par2File := findPar2File(downloadedFiles)
|
||||
if par2File != "" {
|
||||
var repairable *Par2RepairableError
|
||||
err := Par2Verify(par2File)
|
||||
if err != nil {
|
||||
if _, ok := err.(*Par2RepairableError); ok {
|
||||
log.Printf("[usenet] attempting par2 repair...")
|
||||
if repairErr := Par2Repair(par2File); repairErr != nil {
|
||||
log.Printf("[usenet] par2 repair failed: %v", repairErr)
|
||||
} else {
|
||||
result.Repaired = true
|
||||
}
|
||||
} else {
|
||||
log.Printf("[usenet] par2 verification error: %v", err)
|
||||
switch {
|
||||
case err == nil:
|
||||
// Verified OK — nothing to surface.
|
||||
case errors.Is(err, ErrPar2NotInstalled):
|
||||
result.VerifyNote = "par2 parity present but `par2` is not installed — delivered UNVERIFIED (install par2cmdline to enable verification/repair)"
|
||||
log.Printf("[usenet] WARNING: %s", result.VerifyNote)
|
||||
case errors.As(err, &repairable):
|
||||
log.Printf("[usenet] par2: corruption detected, attempting repair...")
|
||||
repairErr := Par2Repair(par2File)
|
||||
switch {
|
||||
case repairErr == nil:
|
||||
result.Repaired = true
|
||||
log.Printf("[usenet] par2: repair successful")
|
||||
case errors.Is(repairErr, ErrPar2NotInstalled):
|
||||
result.VerifyNote = "par2 corruption detected but `par2` is not installed — cannot repair, delivered POSSIBLY CORRUPT"
|
||||
log.Printf("[usenet] WARNING: %s", result.VerifyNote)
|
||||
default:
|
||||
result.VerifyNote = fmt.Sprintf("par2 repair failed — file may be corrupt: %v", repairErr)
|
||||
log.Printf("[usenet] WARNING: %s", result.VerifyNote)
|
||||
}
|
||||
default:
|
||||
result.VerifyNote = fmt.Sprintf("par2 verification error — file may be corrupt: %v", err)
|
||||
log.Printf("[usenet] WARNING: %s", result.VerifyNote)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,38 @@ import (
|
|||
"testing"
|
||||
)
|
||||
|
||||
// TestProcess_Par2MissingSurfaced verifies that when parity is present but the
|
||||
// par2 binary is missing, Process does NOT silently report success: it surfaces
|
||||
// the degraded state via VerifyNote and leaves Verified false (while still
|
||||
// delivering the file).
|
||||
func TestProcess_Par2MissingSurfaced(t *testing.T) {
|
||||
orig := par2Lookup
|
||||
par2Lookup = func() bool { return false }
|
||||
defer func() { par2Lookup = orig }()
|
||||
|
||||
dir := t.TempDir()
|
||||
par2Path := filepath.Join(dir, "release.par2")
|
||||
if err := os.WriteFile(par2Path, []byte("fake parity"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
vid := filepath.Join(dir, "movie.mkv")
|
||||
if err := os.WriteFile(vid, []byte("video data"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
files := map[string]string{"release.par2": par2Path, "movie.mkv": vid}
|
||||
|
||||
res, err := Process(dir, files, Options{})
|
||||
if err != nil {
|
||||
t.Fatalf("Process: %v", err)
|
||||
}
|
||||
if res.VerifyNote == "" {
|
||||
t.Error("VerifyNote must be set (not silent) when par2 is missing")
|
||||
}
|
||||
if res.FinalPath != vid {
|
||||
t.Errorf("FinalPath = %q, want %q (file still delivered)", res.FinalPath, vid)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindPar2File(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue