2026-03-28 21:12:12 +01:00
|
|
|
package postprocess
|
|
|
|
|
|
|
|
|
|
import (
|
2026-06-01 15:52:54 +02:00
|
|
|
"errors"
|
2026-03-28 21:12:12 +01:00
|
|
|
"fmt"
|
|
|
|
|
"log"
|
|
|
|
|
"os/exec"
|
|
|
|
|
"strings"
|
|
|
|
|
)
|
|
|
|
|
|
2026-06-01 15:52:54 +02:00
|
|
|
// 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 {
|
2026-03-28 21:12:12 +01:00
|
|
|
_, err := exec.LookPath("par2")
|
|
|
|
|
return err == nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 15:52:54 +02:00
|
|
|
// 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.
|
2026-03-28 21:12:12 +01:00
|
|
|
func Par2Verify(par2File string) error {
|
|
|
|
|
if !Par2Available() {
|
2026-06-01 15:52:54 +02:00
|
|
|
return ErrPar2NotInstalled
|
2026-03-28 21:12:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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() {
|
2026-06-01 15:52:54 +02:00
|
|
|
return ErrPar2NotInstalled
|
2026-03-28 21:12:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|