unarr/internal/usenet/postprocess/par2.go
Deivid Soto 3d51013935 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).
2026-06-01 15:52:54 +02:00

77 lines
2.3 KiB
Go

package postprocess
import (
"errors"
"fmt"
"log"
"os/exec"
"strings"
)
// 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
}
// 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() {
return ErrPar2NotInstalled
}
cmd := exec.Command("par2", "verify", par2File)
output, err := cmd.CombinedOutput()
if err != nil {
outStr := string(output)
// Check if repair is possible
if strings.Contains(outStr, "Repair is possible") {
return &Par2RepairableError{Par2File: par2File}
}
if strings.Contains(outStr, "Repair is not possible") {
return fmt.Errorf("par2: verification failed and repair not possible:\n%s", outStr)
}
return fmt.Errorf("par2 verify: %w\n%s", err, outStr)
}
log.Printf("[usenet] par2: verification OK")
return nil
}
// Par2Repair attempts to repair files using par2 parity data.
func Par2Repair(par2File string) error {
if !Par2Available() {
return ErrPar2NotInstalled
}
cmd := exec.Command("par2", "repair", par2File)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("par2 repair: %w\n%s", err, output)
}
log.Printf("[usenet] par2: repair successful")
return nil
}
// Par2RepairableError indicates verification failed but repair is possible.
type Par2RepairableError struct {
Par2File string
}
func (e *Par2RepairableError) Error() string {
return fmt.Sprintf("par2: verification failed, repair possible: %s", e.Par2File)
}