Update golangci-lint to v2.12.2 with canonical config (closes #60)
check / check (push) Has been cancelled
check / check (push) Has been cancelled
Adopts golangci-lint v2.12.2 and the canonical .golangci.yml (default: all), and fixes all resulting findings across the tree. Two intended behavior changes: absent MFFilePath.Mtime is handled explicitly in freshen, list and export rather than dereferenced (main panicked); gpg positional key IDs now follow an explicit -- end-of-options marker. All twelve reworded user-visible error messages restored to byte-identical parity with main and pinned by tests.
This commit was merged in pull request #59.
This commit is contained in:
+106
-78
@@ -14,6 +14,8 @@ import (
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
var errNoSigningPubKey = errors.New("manifest has no signing public key")
|
||||
|
||||
// Result represents the outcome of checking a single file.
|
||||
type Result struct {
|
||||
Path RelFilePath // Relative path from manifest
|
||||
@@ -24,6 +26,7 @@ type Result struct {
|
||||
// Status represents the verification status of a file.
|
||||
type Status int
|
||||
|
||||
// Verification result statuses reported for each checked file.
|
||||
const (
|
||||
StatusOK Status = iota // File matches manifest (size and hash verified)
|
||||
StatusMissing // File not found on disk
|
||||
@@ -70,7 +73,8 @@ type Checker struct {
|
||||
fs afero.Fs
|
||||
// manifestPaths is a set of paths in the manifest for quick lookup
|
||||
manifestPaths map[RelFilePath]struct{}
|
||||
// manifestRelPath is the relative path of the manifest file from basePath (for exclusion)
|
||||
// manifestRelPath is the relative path of the manifest file from
|
||||
// basePath (for exclusion)
|
||||
manifestRelPath RelFilePath
|
||||
// signature info from the manifest
|
||||
signature []byte
|
||||
@@ -97,9 +101,10 @@ func NewChecker(manifestPath string, basePath string, fs afero.Fs) (*Checker, er
|
||||
}
|
||||
|
||||
files := m.Files()
|
||||
|
||||
manifestPaths := make(map[RelFilePath]struct{}, len(files))
|
||||
for _, f := range files {
|
||||
manifestPaths[RelFilePath(f.Path)] = struct{}{}
|
||||
manifestPaths[RelFilePath(f.GetPath())] = struct{}{}
|
||||
}
|
||||
|
||||
// Compute manifest's relative path from basePath for exclusion in FindExtraFiles
|
||||
@@ -107,6 +112,7 @@ func NewChecker(manifestPath string, basePath string, fs afero.Fs) (*Checker, er
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
manifestRel, err := filepath.Rel(abs, absManifest)
|
||||
if err != nil {
|
||||
manifestRel = ""
|
||||
@@ -118,9 +124,9 @@ func NewChecker(manifestPath string, basePath string, fs afero.Fs) (*Checker, er
|
||||
fs: fs,
|
||||
manifestPaths: manifestPaths,
|
||||
manifestRelPath: RelFilePath(manifestRel),
|
||||
signature: m.pbOuter.Signature,
|
||||
signer: m.pbOuter.Signer,
|
||||
signingPubKey: m.pbOuter.SigningPubKey,
|
||||
signature: m.pbOuter.GetSignature(),
|
||||
signer: m.pbOuter.GetSigner(),
|
||||
signingPubKey: m.pbOuter.GetSigningPubKey(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -133,8 +139,9 @@ func (c *Checker) FileCount() FileCount {
|
||||
func (c *Checker) TotalBytes() FileSize {
|
||||
var total FileSize
|
||||
for _, f := range c.files {
|
||||
total += FileSize(f.Size)
|
||||
total += FileSize(f.GetSize())
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
@@ -148,7 +155,8 @@ func (c *Checker) Signer() []byte {
|
||||
return c.signer
|
||||
}
|
||||
|
||||
// SigningPubKey returns the signing public key if the manifest is signed, nil otherwise.
|
||||
// SigningPubKey returns the signing public key if the manifest is signed,
|
||||
// nil otherwise.
|
||||
func (c *Checker) SigningPubKey() []byte {
|
||||
return c.signingPubKey
|
||||
}
|
||||
@@ -158,8 +166,9 @@ func (c *Checker) SigningPubKey() []byte {
|
||||
// returns its actual fingerprint from the key material itself.
|
||||
func (c *Checker) ExtractEmbeddedSigningKeyFP() (string, error) {
|
||||
if len(c.signingPubKey) == 0 {
|
||||
return "", errors.New("manifest has no signing public key")
|
||||
return "", errNoSigningPubKey
|
||||
}
|
||||
|
||||
return gpgExtractPubKeyFingerprint(c.signingPubKey)
|
||||
}
|
||||
|
||||
@@ -167,10 +176,15 @@ func (c *Checker) ExtractEmbeddedSigningKeyFP() (string, error) {
|
||||
// Results are sent to the results channel as files are checked.
|
||||
// Progress updates are sent to the progress channel approximately once per second.
|
||||
// Both channels are closed when the method returns.
|
||||
func (c *Checker) Check(ctx context.Context, results chan<- Result, progress chan<- CheckStatus) error {
|
||||
func (c *Checker) Check(
|
||||
ctx context.Context,
|
||||
results chan<- Result,
|
||||
progress chan<- CheckStatus,
|
||||
) error {
|
||||
if results != nil {
|
||||
defer close(results)
|
||||
}
|
||||
|
||||
if progress != nil {
|
||||
defer close(progress)
|
||||
}
|
||||
@@ -178,9 +192,11 @@ func (c *Checker) Check(ctx context.Context, results chan<- Result, progress cha
|
||||
totalFiles := FileCount(len(c.files))
|
||||
totalBytes := c.TotalBytes()
|
||||
|
||||
var checkedFiles FileCount
|
||||
var checkedBytes FileSize
|
||||
var failures FileCount
|
||||
var (
|
||||
checkedFiles FileCount
|
||||
checkedBytes FileSize
|
||||
failures FileCount
|
||||
)
|
||||
|
||||
startTime := time.Now()
|
||||
lastProgressTime := time.Now()
|
||||
@@ -196,6 +212,7 @@ func (c *Checker) Check(ctx context.Context, results chan<- Result, progress cha
|
||||
if result.Status != StatusOK {
|
||||
failures++
|
||||
}
|
||||
|
||||
checkedFiles++
|
||||
|
||||
if results != nil {
|
||||
@@ -205,19 +222,12 @@ func (c *Checker) Check(ctx context.Context, results chan<- Result, progress cha
|
||||
// Send progress at most once per second (rate-limited)
|
||||
if progress != nil {
|
||||
now := time.Now()
|
||||
|
||||
isLast := checkedFiles == totalFiles
|
||||
if isLast || now.Sub(lastProgressTime) >= time.Second {
|
||||
elapsed := time.Since(startTime)
|
||||
var bytesPerSec float64
|
||||
var eta time.Duration
|
||||
|
||||
if elapsed > 0 && checkedBytes > 0 {
|
||||
bytesPerSec = float64(checkedBytes) / elapsed.Seconds()
|
||||
remainingBytes := totalBytes - checkedBytes
|
||||
if bytesPerSec > 0 {
|
||||
eta = time.Duration(float64(remainingBytes)/bytesPerSec) * time.Second
|
||||
}
|
||||
}
|
||||
bytesPerSec, eta := computeRateETA(
|
||||
time.Since(startTime), checkedBytes, totalBytes,
|
||||
)
|
||||
|
||||
sendCheckStatus(progress, CheckStatus{
|
||||
TotalFiles: totalFiles,
|
||||
@@ -228,6 +238,7 @@ func (c *Checker) Check(ctx context.Context, results chan<- Result, progress cha
|
||||
ETA: eta,
|
||||
Failures: failures,
|
||||
})
|
||||
|
||||
lastProgressTime = now
|
||||
}
|
||||
}
|
||||
@@ -236,59 +247,6 @@ func (c *Checker) Check(ctx context.Context, results chan<- Result, progress cha
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Checker) checkFile(entry *MFFilePath, checkedBytes *FileSize) Result {
|
||||
absPath := filepath.Join(string(c.basePath), entry.Path)
|
||||
relPath := RelFilePath(entry.Path)
|
||||
|
||||
// Check if file exists
|
||||
info, err := c.fs.Stat(absPath)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) || errors.Is(err, afero.ErrFileNotFound) {
|
||||
return Result{Path: relPath, Status: StatusMissing, Message: "file not found"}
|
||||
}
|
||||
return Result{Path: relPath, Status: StatusError, Message: err.Error()}
|
||||
}
|
||||
|
||||
// Check size
|
||||
if info.Size() != entry.Size {
|
||||
*checkedBytes += FileSize(info.Size())
|
||||
return Result{
|
||||
Path: relPath,
|
||||
Status: StatusSizeMismatch,
|
||||
Message: "size mismatch",
|
||||
}
|
||||
}
|
||||
|
||||
// Open and hash file
|
||||
f, err := c.fs.Open(absPath)
|
||||
if err != nil {
|
||||
return Result{Path: relPath, Status: StatusError, Message: err.Error()}
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
h := sha256.New()
|
||||
n, err := io.Copy(h, f)
|
||||
if err != nil {
|
||||
return Result{Path: relPath, Status: StatusError, Message: err.Error()}
|
||||
}
|
||||
*checkedBytes += FileSize(n)
|
||||
|
||||
// Encode as multihash and compare
|
||||
computed, err := multihash.Encode(h.Sum(nil), multihash.SHA2_256)
|
||||
if err != nil {
|
||||
return Result{Path: relPath, Status: StatusError, Message: err.Error()}
|
||||
}
|
||||
|
||||
// Check against all hashes in manifest (at least one must match)
|
||||
for _, hash := range entry.Hashes {
|
||||
if bytes.Equal(computed, hash.MultiHash) {
|
||||
return Result{Path: relPath, Status: StatusOK}
|
||||
}
|
||||
}
|
||||
|
||||
return Result{Path: relPath, Status: StatusHashMismatch, Message: "hash mismatch"}
|
||||
}
|
||||
|
||||
// FindExtraFiles walks the filesystem and reports files not in the manifest.
|
||||
// Results are sent to the results channel. The channel is closed when done.
|
||||
// Hidden files/directories (starting with .) are skipped, as they are excluded
|
||||
@@ -298,7 +256,7 @@ func (c *Checker) FindExtraFiles(ctx context.Context, results chan<- Result) err
|
||||
defer close(results)
|
||||
}
|
||||
|
||||
return afero.Walk(c.fs, string(c.basePath), func(walkPath string, info os.FileInfo, err error) error {
|
||||
walkFn := func(walkPath string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -320,6 +278,7 @@ func (c *Checker) FindExtraFiles(ctx context.Context, results chan<- Result) err
|
||||
if info.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -347,7 +306,75 @@ func (c *Checker) FindExtraFiles(ctx context.Context, results chan<- Result) err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
return afero.Walk(c.fs, string(c.basePath), walkFn)
|
||||
}
|
||||
|
||||
func (c *Checker) checkFile(entry *MFFilePath, checkedBytes *FileSize) Result {
|
||||
absPath := filepath.Join(string(c.basePath), entry.GetPath())
|
||||
relPath := RelFilePath(entry.GetPath())
|
||||
|
||||
// Check if file exists
|
||||
info, err := c.fs.Stat(absPath)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) || errors.Is(err, afero.ErrFileNotFound) {
|
||||
return Result{
|
||||
Path: relPath,
|
||||
Status: StatusMissing,
|
||||
Message: "file not found",
|
||||
}
|
||||
}
|
||||
|
||||
return Result{Path: relPath, Status: StatusError, Message: err.Error()}
|
||||
}
|
||||
|
||||
// Check size
|
||||
if info.Size() != entry.GetSize() {
|
||||
*checkedBytes += FileSize(info.Size())
|
||||
|
||||
return Result{
|
||||
Path: relPath,
|
||||
Status: StatusSizeMismatch,
|
||||
Message: "size mismatch",
|
||||
}
|
||||
}
|
||||
|
||||
// Open and hash file
|
||||
f, err := c.fs.Open(absPath)
|
||||
if err != nil {
|
||||
return Result{Path: relPath, Status: StatusError, Message: err.Error()}
|
||||
}
|
||||
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
h := sha256.New()
|
||||
|
||||
n, err := io.Copy(h, f)
|
||||
if err != nil {
|
||||
return Result{Path: relPath, Status: StatusError, Message: err.Error()}
|
||||
}
|
||||
|
||||
*checkedBytes += FileSize(n)
|
||||
|
||||
// Encode as multihash and compare
|
||||
computed, err := multihash.Encode(h.Sum(nil), multihash.SHA2_256)
|
||||
if err != nil {
|
||||
return Result{Path: relPath, Status: StatusError, Message: err.Error()}
|
||||
}
|
||||
|
||||
// Check against all hashes in manifest (at least one must match)
|
||||
for _, hash := range entry.GetHashes() {
|
||||
if bytes.Equal(computed, hash.GetMultiHash()) {
|
||||
return Result{Path: relPath, Status: StatusOK}
|
||||
}
|
||||
}
|
||||
|
||||
return Result{
|
||||
Path: relPath,
|
||||
Status: StatusHashMismatch,
|
||||
Message: "hash mismatch",
|
||||
}
|
||||
}
|
||||
|
||||
// sendCheckStatus sends a status update without blocking.
|
||||
@@ -355,6 +382,7 @@ func sendCheckStatus(ch chan<- CheckStatus, status CheckStatus) {
|
||||
if ch == nil {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case ch <- status:
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user